public
Rubygem
Description: Annotate ActiveRecord models as a gem
Homepage: http://agilewebdevelopment.com/plugins/annotate_models
Clone URL: git://github.com/ctran/annotate_models.git
annotate_models / lib / annotate_models.rb
100644 123 lines (104 sloc) 3.893 kb
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
module AnnotateModels
  MODEL_DIR = "app/models"
  FIXTURE_DIR = "test/fixtures"
  PREFIX = "== Schema Information"
 
  # Simple quoting for the default column value
  def self.quote(value)
    case value
      when NilClass then "NULL"
      when TrueClass then "TRUE"
      when FalseClass then "FALSE"
      when Float, Fixnum, Bignum then value.to_s
      # BigDecimals need to be output in a non-normalized form and quoted.
      when BigDecimal then value.to_s('F')
      else
        value.inspect
    end
  end
 
  # Use the column information in an ActiveRecord class
  # to create a comment block containing a line for
  # each column. The line contains the column name,
  # the type (and length), and any optional attributes
  def self.get_schema_info(klass, header)
    info = "# #{header}\n#\n"
    info << "# Table name: #{klass.table_name}\n#\n"
 
    max_size = klass.column_names.collect{|name| name.size}.max + 1
    klass.columns.each do |col|
      attrs = []
      attrs << "default(#{quote(col.default)})" if col.default
      attrs << "not null" unless col.null
      attrs << "primary key" if col.name == klass.primary_key
 
      col_type = col.type.to_s
      if col_type == "decimal"
        col_type << "(#{col.precision}, #{col.scale})"
      else
        col_type << "(#{col.limit})" if col.limit
      end
      info << sprintf("# %-#{max_size}.#{max_size}s:%-15.15s %s\n", col.name, col_type, attrs.join(", "))
    end
 
    info << "#\n\n"
  end
 
  # Add a schema block to a file. If the file already contains
  # a schema info block (a comment starting
  # with "Schema as of ..."), remove it first.
 
  def self.annotate_one_file(file_name, info_block)
    if File.exist?(file_name)
      content = File.read(file_name)
 
      # Remove old schema info
      content.sub!(/^# #{PREFIX}.*?\n(#.*\n)*\n/, '')
 
      # Write it back
      File.open(file_name, "w") { |f| f.puts info_block + content }
    end
  end
 
  # Given the name of an ActiveRecord class, create a schema
  # info block (basically a comment containing information
  # on the columns and their types) and put it at the front
  # of the model and fixture source files.
 
  def self.annotate(klass, header)
    info = get_schema_info(klass, header)
 
    model_file_name = File.join(MODEL_DIR, klass.name.underscore + ".rb")
    annotate_one_file(model_file_name, info)
 
    fixture_file_name = File.join(FIXTURE_DIR, klass.table_name + ".yml")
    annotate_one_file(fixture_file_name, info)
  end
 
  # Return a list of the model files to annotate. If we have
  # command line arguments, they're assumed to be either
  # the underscore or CamelCase versions of model names.
  # Otherwise we take all the model files in the
  # app/models directory.
  def self.get_model_names
    models = ARGV.dup
    models.shift
 
    if models.empty?
      Dir.chdir(MODEL_DIR) do
        models = Dir["**/*.rb"]
      end
    end
    models
  end
 
  # We're passed a name of things that might be
  # ActiveRecord models. If we can find the class, and
  # if its a subclass of ActiveRecord::Base,
  # then pas it to the associated block
 
  def self.do_annotations
    header = PREFIX.dup
    version = ActiveRecord::Migrator.current_version rescue 0
    if version > 0
      header << "\n# Schema version: #{version}"
    end
 
    annotated = []
    self.get_model_names.each do |m|
      class_name = m.sub(/\.rb$/,'').camelize
      begin
        klass = class_name.split('::').inject(Object){ |klass,part| klass.const_get(part) }
        if klass < ActiveRecord::Base && !klass.abstract_class?
          annotated << class_name
          self.annotate(klass, header)
        end
      rescue Exception => e
        puts "Unable to annotate #{class_name}: #{e.message}"
      end
 
    end
    puts "Annotated #{annotated.join(', ')}"
  end
end