require 'date' require 'time' require 'bigdecimal' module DataMapper # :include:QUICKLINKS # # = Properties # Properties for a model are not derived from a database structure, but # instead explicitly declared inside your model class definitions. These # properties then map (or, if using automigrate, generate) fields in your # repository/database. # # If you are coming to DataMapper from another ORM framework, such as # ActiveRecord, this is a fundamental difference in thinking. However, there # are several advantages to defining your properties in your models: # # * information about your model is centralized in one place: rather than # having to dig out migrations, xml or other configuration files. # * having information centralized in your models, encourages you and the # developers on your team to take a model-centric view of development. # * it provides the ability to use Ruby's access control functions. # * and, because DataMapper only cares about properties explicitly defined in # your models, DataMapper plays well with legacy databases, and shares # databases easily with other applications. # # == Declaring Properties # Inside your class, you call the property method for each property you want # to add. The only two required arguments are the name and type, everything # else is optional. # # class Post # include DataMapper::Resource # property :title, String, :nullable => false # # Cannot be null # property :publish, TrueClass, :default => false # # Default value for new records is false # end # # By default, DataMapper supports the following primitive types: # # * TrueClass, Boolean # * String # * Text (limit of 65k characters by default) # * Float # * Integer # * BigDecimal # * DateTime # * Date # * Time # * Object (marshalled out during serialization) # * Class (datastore primitive is the same as String. Used for Inheritance) # # For more information about available Types, see DataMapper::Type # # == Limiting Access # Property access control is uses the same terminology Ruby does. Properties # are public by default, but can also be declared private or protected as # needed (via the :accessor option). # # class Post # include DataMapper::Resource # property :title, String, :accessor => :private # # Both reader and writer are private # property :body, Text, :accessor => :protected # # Both reader and writer are protected # end # # Access control is also analogous to Ruby accessors and mutators, and can # be declared using :reader and :writer, in addition to :accessor. # # class Post # include DataMapper::Resource # # property :title, String, :writer => :private # # Only writer is private # # property :tags, String, :reader => :protected # # Only reader is protected # end # # == Overriding Accessors # The accessor for any property can be overridden in the same manner that Ruby # class accessors can be. After the property is defined, just add your custom # accessor: # # class Post # include DataMapper::Resource # property :title, String # # def title=(new_title) # raise ArgumentError if new_title != 'Luke is Awesome' # @title = new_title # end # end # # == Lazy Loading # By default, some properties are not loaded when an object is fetched in # DataMapper. These lazily loaded properties are fetched on demand when their # accessor is called for the first time (as it is often unnecessary to # instantiate -every- property -every- time an object is loaded). For # instance, DataMapper::Types::Text fields are lazy loading by default, # although you can over-ride this behavior if you wish: # # Example: # # class Post # include DataMapper::Resource # property :title, String # Loads normally # property :body, DataMapper::Types::Text # Is lazily loaded by default # end # # If you want to over-ride the lazy loading on any field you can set it to a # context or false to disable it with the :lazy option. Contexts allow # multipule lazy properties to be loaded at one time. If you set :lazy to # true, it is placed in the :default context # # class Post # include DataMapper::Resource # # property :title, String # # Loads normally # # property :body, DataMapper::Types::Text, :lazy => false # # The default is now over-ridden # # property :comment, String, lazy => [:detailed] # # Loads in the :detailed context # # property :author, String, lazy => [:summary,:detailed] # # Loads in :summary & :detailed context # end # # Delaying the request for lazy-loaded attributes even applies to objects # accessed through associations. In a sense, DataMapper anticipates that # you will likely be iterating over objects in associations and rolls all # of the load commands for lazy-loaded properties into one request from # the database. # # Example: # # Widget[1].components # # loads when the post object is pulled from database, by default # # Widget[1].components.first.body # # loads the values for the body property on all objects in the # # association, rather than just this one. # # Widget[1].components.first.comment # # loads both comment and author for all objects in the association # # since they are both in the :detailed context # # == Keys # Properties can be declared as primary or natural keys on a table. # You should a property as the primary key of the table: # # Examples: # # property :id, Serial # auto-incrementing key # property :legacy_pk, String, :key => true # 'natural' key # # This is roughly equivalent to ActiveRecord's set_primary_key, # though non-integer data types may be used, thus DataMapper supports natural # keys. When a property is declared as a natural key, accessing the object # using the indexer syntax Class[key] remains valid. # # User[1] # # when :id is the primary key on the users table # User['bill'] # # when :name is the primary (natural) key on the users table # # == Indeces # You can add indeces for your properties by using the :index # option. If you use true as the option value, the index will be # automatically named. If you want to name the index yourself, use a symbol # as the value. # # property :last_name, String, :index => true # property :first_name, String, :index => :name # # You can create multi-column composite indeces by using the same symbol in # all the columns belonging to the index. The columns will appear in the # index in the order they are declared. # # property :last_name, String, :index => :name # property :first_name, String, :index => :name # # => index on (last_name, first_name) # # If you want to make the indeces unique, use :unique_index instead # of :index # # == Inferred Validations # If you require the dm-validations plugin, auto-validations will # automatically be mixed-in in to your model classes: # validation rules that are inferred when properties are declared with # specific column restrictions. # # class Post # include DataMapper::Resource # # property :title, String, :length => 250 # # => infers 'validates_length :title, # :minimum => 0, :maximum => 250' # # property :title, String, :nullable => false # # => infers 'validates_present :title # # property :email, String, :format => :email_address # # => infers 'validates_format :email, :with => :email_address # # property :title, String, :length => 255, :nullable => false # # => infers both 'validates_length' as well as # # 'validates_present' # # better: property :title, String, :length => 1..255 # # end # # This functionality is available with the dm-validations gem, part of the # dm-more bundle. For more information about validations, check the # documentation for dm-validations. # # == Embedded Values # As an alternative to extraneous has_one relationships, consider using an # EmbeddedValue. # # == Misc. Notes # * Properties declared as strings will default to a length of 50, rather than # 255 (typical max varchar column size). To overload the default, pass # :length => 255 or :length => 0..255. Since DataMapper # does not introspect for properties, this means that legacy database tables # may need their String columns defined with a :length so # that DM does not apply an un-needed length validation, or allow overflow. # * You may declare a Property with the data-type of Class. # see SingleTableInheritance for more on how to use Class columns. class Property include Assertions # NOTE: check is only for psql, so maybe the postgres adapter should # define its own property options. currently it will produce a warning tho # since PROPERTY_OPTIONS is a constant # # NOTE: PLEASE update PROPERTY_OPTIONS in DataMapper::Type when updating # them here PROPERTY_OPTIONS = [ :accessor, :reader, :writer, :lazy, :default, :nullable, :key, :serial, :field, :size, :length, :format, :index, :unique_index, :check, :ordinal, :auto_validation, :validates, :unique, :track, :precision, :scale ] # FIXME: can we pull the keys from # DataMapper::Adapters::DataObjectsAdapter::TYPES # for this? TYPES = [ TrueClass, String, DataMapper::Types::Text, Float, Integer, BigDecimal, DateTime, Date, Time, Object, Class, DataMapper::Types::Discriminator, DataMapper::Types::Serial ] IMMUTABLE_TYPES = [ TrueClass, Float, Integer, BigDecimal] VISIBILITY_OPTIONS = [ :public, :protected, :private ] DEFAULT_LENGTH = 50 DEFAULT_PRECISION = 10 DEFAULT_SCALE_BIGDECIMAL = 0 DEFAULT_SCALE_FLOAT = nil attr_reader :primitive, :model, :name, :instance_variable_name, :type, :reader_visibility, :writer_visibility, :getter, :options, :default, :precision, :scale, :track, :extra_options # Supplies the field in the data-store which the property corresponds to # # @return name of field in data-store # - # @api semi-public def field(repository_name = nil) @field || fields[repository_name || model.repository_name] end def unique @unique ||= @options.fetch(:unique, @serial || @key || false) end def hash if @custom && !@bound @type.bind(self) @bound = true end return @model.hash + @name.hash end def eql?(o) if o.is_a?(Property) return o.model == @model && o.name == @name else return false end end def length @length.is_a?(Range) ? @length.max : @length end alias size length def index @index end def unique_index @unique_index end # Returns whether or not the property is to be lazy-loaded # # @return whether or not the property is to be # lazy-loaded # - # @api public def lazy? @lazy end # Returns whether or not the property is a key or a part of a key # # @return whether the property is a key or a part of # a key #- # @api public def key? @key end # Returns whether or not the property is "serial" (auto-incrementing) # # @return whether or not the property is "serial" #- # @api public def serial? @serial end # Returns whether or not the property can accept 'nil' as it's value # # @return whether or not the property can accept 'nil' #- # @api public def nullable? @nullable end def custom? @custom end # Provides a standardized getter method for the property # # @raise "+resource+ should be a DataMapper::Resource, but was ...." #- # @api private def get(resource) new_record = resource.new_record? lazy_load(resource) unless new_record || resource.attribute_loaded?(name) value = get!(resource) case track when :hash resource.original_values[name] = value.dup.hash unless resource.original_values.has_key?(name) rescue value.hash when :get resource.original_values[name] = value.dup unless resource.original_values.has_key?(name) rescue value end if value.nil? && new_record && !options[:default].nil? && !resource.attribute_loaded?(name) value = default_for(resource) set(resource, value) end value end def get!(resource) resource.instance_variable_get(instance_variable_name) end # Provides a standardized setter method for the property # # @raise "+resource+ should be a DataMapper::Resource, but was ...." #- # @api private def set(resource, value) lazy_load(resource) unless resource.new_record? || resource.attribute_loaded?(name) new_value = typecast(value) old_value = get!(resource) # skip setting the property if the new value is equal # to the old value, and the old value was defined return if new_value == old_value && resource.attribute_loaded?(name) resource.original_values[name] = old_value unless resource.original_values.has_key?(name) set!(resource, new_value) end def set!(resource, value) resource.instance_variable_set(instance_variable_name, value) end # Loads lazy columns when get or set is called. #- # @api private def lazy_load(resource) # TODO: refactor this section contexts = if lazy? name else model.properties(resource.repository.name).reject do |property| property.lazy? || resource.attribute_loaded?(property.name) end end resource.send(:lazy_load, contexts) end # typecasts values into a primitive # # @return the primitive data-type, defaults to TrueClass #- # @api private def typecast(value) return type.typecast(value, self) if type.respond_to?(:typecast) return value if value.kind_of?(primitive) || value.nil? begin if primitive == TrueClass then %w[ true 1 t ].include?(value.to_s.downcase) elsif primitive == String then value.to_s elsif primitive == Float then value.to_f elsif primitive == Integer # The simplest possible implementation, i.e. value.to_i, is not # desirable because "junk".to_i gives "0". We want nil instead, # because this makes it clear that the typecast failed. # # After benchmarking, we preferred the current implementation over # these two alternatives: # * Integer(value) rescue nil # * Integer(value_to_s =~ /(\d+)/ ? $1 : value_to_s) rescue nil value_to_i = value.to_i if value_to_i == 0 && value != '0' value_to_s = value.to_s begin Integer(value_to_s =~ /^(\d+)/ ? $1 : value_to_s) rescue ArgumentError nil end else value_to_i end elsif primitive == BigDecimal then BigDecimal(value.to_s) elsif primitive == DateTime then typecast_to_datetime(value) elsif primitive == Date then typecast_to_date(value) elsif primitive == Time then typecast_to_time(value) elsif primitive == Class then self.class.find_const(value) else value end rescue value end end def default_for(resource) @default.respond_to?(:call) ? @default.call(resource, self) : @default end def inspect "#" end private def initialize(model, name, type, options = {}) assert_kind_of 'model', model, Model assert_kind_of 'name', name, Symbol assert_kind_of 'type', type, Class if Fixnum == type # It was decided that Integer is a more expressively names class to # use instead of Fixnum. Fixnum only represents smaller numbers, # so there was some confusion over whether or not it would also # work with Bignum too (it will). Any Integer, which includes # Fixnum and Bignum, can be stored in this property. warn "#{type} properties are deprecated. Please use Integer instead" type = Integer end unless TYPES.include?(type) || (DataMapper::Type > type && TYPES.include?(type.primitive)) raise ArgumentError, "+type+ was #{type.inspect}, which is not a supported type: #{TYPES * ', '}", caller end @extra_options = {} (options.keys - PROPERTY_OPTIONS).each do |key| @extra_options[key] = options.delete(key) end @model = model @name = name.to_s.sub(/\?$/, '').to_sym @type = type @custom = DataMapper::Type > @type @options = @custom ? @type.options.merge(options) : options @instance_variable_name = "@#{@name}" # TODO: This default should move to a DataMapper::Types::Text # Custom-Type and out of Property. @primitive = @options.fetch(:primitive, @type.respond_to?(:primitive) ? @type.primitive : @type) @getter = TrueClass == @primitive ? "#{@name}?".to_sym : @name @field = @options.fetch(:field, nil) @serial = @options.fetch(:serial, false) @key = @options.fetch(:key, @serial || false) @default = @options.fetch(:default, nil) @nullable = @options.fetch(:nullable, @key == false) @index = @options.fetch(:index, false) @unique_index = @options.fetch(:unique_index, false) @lazy = @options.fetch(:lazy, @type.respond_to?(:lazy) ? @type.lazy : false) && !@key @track = @options.fetch(:track) do if @custom && @type.respond_to?(:track) && @type.track @type.track else IMMUTABLE_TYPES.include?(@primitive) ? :set : :get end end # assign attributes per-type if String == @primitive || Class == @primitive @length = @options.fetch(:length, @options.fetch(:size, DEFAULT_LENGTH)) elsif BigDecimal == @primitive || Float == @primitive @precision = @options.fetch(:precision, DEFAULT_PRECISION) default_scale = (Float == @primitive) ? DEFAULT_SCALE_FLOAT : DEFAULT_SCALE_BIGDECIMAL @scale = @options.fetch(:scale, default_scale) # @scale = @options.fetch(:scale, DEFAULT_SCALE_BIGDECIMAL) unless @precision > 0 raise ArgumentError, "precision must be greater than 0, but was #{@precision.inspect}" end if (BigDecimal == @primitive) || (Float == @primitive && !@scale.nil?) unless @scale >= 0 raise ArgumentError, "scale must be equal to or greater than 0, but was #{@scale.inspect}" end unless @precision >= @scale raise ArgumentError, "precision must be equal to or greater than scale, but was #{@precision.inspect} and scale was #{@scale.inspect}" end end end determine_visibility @model.auto_generate_validations(self) if @model.respond_to?(:auto_generate_validations) @model.property_serialization_setup(self) if @model.respond_to?(:property_serialization_setup) end def fields @fields ||= Hash.new { |h,k| h[k] = self.model.field_naming_conventions[k].call(self) } end def determine_visibility # :nodoc: @reader_visibility = @options[:reader] || @options[:accessor] || :public @writer_visibility = @options[:writer] || @options[:accessor] || :public unless VISIBILITY_OPTIONS.include?(@reader_visibility) && VISIBILITY_OPTIONS.include?(@writer_visibility) raise ArgumentError, 'property visibility must be :public, :protected, or :private', caller(2) end end # Typecasts an arbitrary value to a DateTime def typecast_to_datetime(value) case value when Hash then typecast_hash_to_datetime(value) else DateTime.parse(value.to_s) end end # Typecasts an arbitrary value to a Date def typecast_to_date(value) case value when Hash then typecast_hash_to_date(value) else Date.parse(value.to_s) end end # Typecasts an arbitrary value to a Time def typecast_to_time(value) case value when Hash then typecast_hash_to_time(value) else Time.parse(value.to_s) end end def typecast_hash_to_datetime(hash) args = extract_time_args_from_hash(hash, :year, :month, :day, :hour, :min, :sec) DateTime.new(*args) rescue ArgumentError t = typecast_hash_to_time(hash) DateTime.new(t.year, t.month, t.day, t.hour, t.min, t.sec) end def typecast_hash_to_date(hash) args = extract_time_args_from_hash(hash, :year, :month, :day) Date.new(*args) rescue ArgumentError t = typecast_hash_to_time(hash) Date.new(t.year, t.month, t.day) end def typecast_hash_to_time(hash) args = extract_time_args_from_hash(hash, :year, :month, :day, :hour, :min, :sec) Time.local(*args) end # Extracts the given args from the hash. If a value does not exist, it # uses the value of Time.now def extract_time_args_from_hash(hash, *args) now = Time.now args.map { |arg| hash[arg] || hash[arg.to_s] || now.send(arg) } end end # class Property end # module DataMapper