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)

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'

Use Scenarios

There are two basic alternative strategies:

Define schema modifications with migrations first

To help with this strategy, the rake fields:update task can be made to be automatically called after each migration.

To activate this functionality, this must be added to your Rakefile:

require 'modalfields/migrate_tasks'

To alter the DB schema migrations are prepared; when the migrations are executed the field declarations are automatically updated. Further customization of the field declarations can be done before committing the changes.

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…)

Maintain schema via field declarations

If the preferred strategy is to define changes in the field declarations, rake fields:migration can be used to help write the necessary migration.

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.