ModalFields

This is a Rails Plugin to maintain schema information in the models’ definitions. It is a hybrid between HoboFields and model annotators.

It works like other annotators, by adding documentation to the model classes from the DB schema. But the annotations are syntactic Ruby as in HoboFields rather than comments:

class User < ActiveRecord::Base
  fields do
    name :string
    birthdate :date
  end
end

Apart from looking prettier to my eyes, this allows triggering special functionality from the field declarations (such as specifying validations).

Fields that are foreign_keys of belongs_to associations are not annotated; it is assumed that belongs_to and other associations follow the fields block declaration, so the information is readily available.

Primary keys named are also not annotated (unless the ModalFields.show_primary_keys property is changed)

The annotations are kept up to date by the migration tasks (currently only if the plugin is installed under vendor) Comments and validation, etc. specifications modified manually are preserved, at least if the field block syntax is kept as generated (one line per field, one line for the block start and end…)

Custom type fields and hooks can be define in files (e.g. fields.rb) in config/initializers/

Rake Tasks

There’s a couple of Rake tasks:

  • fields:update is what’s called after a migration; it updates the fields blocks in the model class definitions.

  • fields:check shows the difference between the declared fields and the DB schema (what would be modified by fields:update)

Under Rails 2, you need to add this to your Rakefile to make the tasks available:

require 'modalfields/tasks'

Some customization examples:

ModalFields.hook do

  # Declare serialized fields as
  #  field_name :serialized, :class=>Array
  # another option would be: (using the generic hook)
  #  field_name :text, :serialize=>Array
  serialized do |model, declaration|
    model.serialize declaration.name, declaration.attributes[:class].class || Object
    declaration.replace!(:type=>:text).remove_attributes!(:class)
  end

  # Add specific support for date fields (_ui virtual attributes)
  date do |model, declaration|
    model.date_ui declaration.name
  end

  # Add specific support for date and datetime and detect fields with units
  all_fields do |model, declaration|
    date_ui name if [:date, :datetime].include?(declaration.type)
    if ModalSupport::Units.valid_units?(units = declaration.name.to_s.split('_').last)
      prec = {'m'=>1, 'mm'=>0, 'cm'=>0, 'km'=>3}[units] || 0
      magnitude_ui name, prec, units
    end
  end

end

# Spatial Adapter columns: require specific column to declaration conversion and field types

ModalFields.column_to_field_declaration do |column|
  type = column.type.to_sym
  type = column.geometry_type if type==:geometry
  attributes = {}
  attrs = ModalFields.definitions[type]
  attrs.keys.each do |attr|
    v = column.send(attr)
    attributes[attr] = v unless attrs[attr]==v
  end
  ModalFields::FieldDeclaration.new(column.name.to_sym, type, [], attributes)
end

ModalFields.define do
  point               :srid=>nil, :with_z=>false, :with_m=>false, :sql_type=>'POINT'
  line_string         :srid=>nil, :with_z=>false, :with_m=>false, :sql_type=>'LINESTRING'
  polygon             :srid=>nil, :with_z=>false, :with_m=>false, :sql_type=>'POLYGON'
  geometry_collection :srid=>nil, :with_z=>false, :with_m=>false, :sql_type=>'GEOMETRYCOLLECTION'
  multi_point         :srid=>nil, :with_z=>false, :with_m=>false, :sql_type=>'MULTIPOINT'
  multi_line_string   :srid=>nil, :with_z=>false, :with_m=>false, :sql_type=>'MULTILINESTRING'
  multi_polygon       :srid=>nil, :with_z=>false, :with_m=>false, :sql_type=>'MULTIPOLYGON'
  geometry            :srid=>nil, :with_z=>false, :with_m=>false, :sql_type=>nil
end

ModalFields.hook do
  %w{point line_string polygon geometry_collection multi_point multi_line_string multi_polygon}.each do |spatial_type|
    field_type spatial_type.to_sym do |model, declaration|
      declaration.replace!(:type=>:geometry).add!(:sql_type=>spatial_type.upcase.tr('_',''))
    end
  end
end

# Enumerated field with symbolic constants associated (and translated literals) using the enum_id plugin
# Use:
#   enum :name, :values=>{id1=>:first_symbol, id2=>:second_symbol, ...}
# Or: (ids are sequential values starting in 1)
#  enum :name, :values=>[:first_symbol, :second_symbol, ...]
ModalFields.hook do
  enum do |model, declaration|
    values = declaration.attributes[:values]
    if values.kind_of?(Array)
      values = (1..values.size).map_hash{|i| values[i-1]}
    end
    model.enum_id declaration.name, values
    declaration.replace! :type=>:integer, :name=>"#{declaration.name}_id"
  end

  class ModalFields::Declaration
    def enum(*values)
      values = values.first if values.size==1 && values.first.kind_of?(Hash)
      {:values=>values}
    end
  end
end

Copyright © 2011 Javier Goizueta. See LICENSE.txt for further details.