# encoding: utf-8 #-- module RailsERD class Domain # Describes an entity's attribute. Attributes correspond directly to # database columns. class Attribute TIMESTAMP_NAMES = %w{created_at created_on updated_at updated_on} # @private :nodoc: class << self def from_model(domain, model) # @private :nodoc: attributes = model.columns.collect { |column| new(domain, model, column) } attributes.sort! if RailsERD.options[:sort] if RailsERD.options[:prepend_primary] attributes = prepend_primary(model, attributes) end attributes end def prepend_primary(model, attributes) primary_key = ActiveRecord::Base.get_primary_key(model) primary = attributes.index { |column| column.name == primary_key } if primary attributes[primary], attributes[0] = attributes[0], attributes[primary] end attributes end end extend Inspectable inspection_attributes :name, :type attr_reader :column # @private :nodoc: def initialize(domain, model, column) # @private :nodoc: @domain, @model, @column = domain, model, column end # The name of the attribute, equal to the column name. def name column.name end # The type of the attribute, equal to the Rails migration type. Can be any # of +:string+, +:integer+, +:boolean+, +:text+, etc. def type column.type or column.sql_type.downcase.to_sym end # Returns +true+ if this attribute is a content column, that is, if it # is not a primary key, foreign key, timestamp, or inheritance column. def content? !primary_key? and !foreign_key? and !timestamp? and !inheritance? end # Returns +true+ if this attribute is mandatory. Mandatory attributes # either have a presence validation (+validates_presence_of+), or have a # NOT NULL database constraint. def mandatory? !column.null or @model.validators_on(name).map(&:kind).include?(:presence) end def unique? @model.validators_on(name).map(&:kind).include?(:uniqueness) end # Returns +true+ if this attribute is the primary key of the entity. def primary_key? @model.primary_key.to_s == name.to_s end # Returns +true+ if this attribute is used as a foreign key for any # relationship. def foreign_key? @domain.relationships_by_entity_name(@model.name).map(&:associations).flatten.map { |associaton| associaton.send(Domain.foreign_key_method_name) }.include?(name) end # Returns +true+ if this attribute is used for single table inheritance. # These attributes are typically named +type+. def inheritance? @model.inheritance_column == name end # Returns +true+ if this attribute is one of the standard 'magic' Rails # timestamp columns, being +created_at+, +updated_at+, +created_on+ or # +updated_on+. def timestamp? TIMESTAMP_NAMES.include? name end def <=>(other) # @private :nodoc: name <=> other.name end def to_s # @private :nodoc: name end # Returns a description of the attribute type. If the attribute has # a non-standard limit or if it is mandatory, this information is included. # # Example output: # :integer:: integer # :string, :limit => 255:: string # :string, :limit => 128:: string (128) # :decimal, :precision => 5, :scale => 2/tt>:: decimal (5,2) # :boolean, :null => false:: boolean * def type_description type.to_s.tap do |desc| desc << " #{limit_description}" if limit_description desc << " ∗" if mandatory? && !primary_key? # Add a hair space + low asterisk (Unicode characters) desc << " U" if unique? && !primary_key? && !foreign_key? # Add U if unique but non-key desc << " PK" if primary_key? desc << " FK" if foreign_key? end end # Returns any non-standard limit for this attribute. If a column has no # limit or uses a default database limit, this method returns +nil+. def limit return column.limit.to_i if column.limit != native_type[:limit] and column.limit.respond_to?(:to_i) column.precision.to_i if column.precision != native_type[:precision] and column.precision.respond_to?(:to_i) end # Returns any non-standard scale for this attribute (decimal types only). def scale return column.scale.to_i if column.scale != native_type[:scale] and column.scale.respond_to?(:to_i) 0 if column.precision end # Returns a string that describes the limit for this attribute, such as # +(128)+, or +(5,2)+ for decimal types. Returns nil if no non-standard # limit was set. def limit_description # @private :nodoc: return "(#{limit},#{scale})" if limit and scale return "(#{limit})" if limit end private def native_type @model.connection.native_database_types[type] or {} end end end end