Module: AnnotateModels

Defined in:
lib/annotate_models.rb

Constant Summary collapse

PREFIX_AT_BOTTOM =
"== Schema Info"
SUFFIX_AT_BOTTOM =
""
PREFIX_ON_TOP =
"== Schema Info =="
SUFFIX_ON_TOP =
"=================\n# "

Class Method Summary collapse

Class Method Details

.annotate(klass) ⇒ Object

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.



171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
# File 'lib/annotate_models.rb', line 171

def self.annotate(klass)
  info = get_schema_info(klass)
  model_name = klass.name.underscore
  fixtures_name = "#{klass.table_name}.yml"

  [
    File.join(self.model_dir,     "#{model_name}.rb"),      # model
    File.join(UNIT_TEST_DIR,      "#{model_name}_test.rb"), # test
    File.join(FIXTURES_DIR,       fixtures_name),           # fixture
    File.join(SPEC_MODEL_DIR,     "#{model_name}_spec.rb"), # spec
    File.join(SPEC_FIXTURES_DIR,  fixtures_name),           # spec fixture
    Rails.root.join(        'test', 'factories.rb'),  # factories file
    Rails.root.join(        'spec', 'factories.rb'),  # factories file
  ].each { |file| annotate_one_file(file, info) }
end

.annotate_column(col, klass, max_size) ⇒ Object



100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
# File 'lib/annotate_models.rb', line 100

def self.annotate_column(col, klass, max_size)
  col_type = col.type.to_s
  attrs = []
  attrs << "not null" unless col.null
  if col.default
    default_value =
      case col_type
      when "decimal" then col.default.to_s.sub(/\.0+\z/, '.0')
      else col.default
      end
    attrs << "default(#{quote(default_value)})"
  end
  attrs << "primary key" if col.name == klass.primary_key

  case col_type
  when "decimal" then
    col_type << "(#{col.precision}, #{col.scale})"
  else
    col_type << "(#{col.limit})" if col.limit
  end
  sprintf("#  %-#{max_size}s:%-15s %s", col.name, col_type, attrs.join(", ")).rstrip
end

.annotate_one_file(file_name, info_block) ⇒ Object

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. Mod to write to the end of the file



138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
# File 'lib/annotate_models.rb', line 138

def self.annotate_one_file(file_name, info_block)
  if File.exist?(file_name)
    content = File.read(file_name)

    encoding_comment = content.scan(/^\#\s*-\*-(.+?)-\*-/).flatten.first
    content.sub!(/^\#\s*-\*-(.+?)-\*-/, '')

    # Remove old schema info
    content.sub!(/(\n)*^# #{PREFIX_ON_TOP}.*?\n(#.*\n)*# #{SUFFIX_ON_TOP}/, '')
    content.sub!(/(\n)*^# #{PREFIX_AT_BOTTOM}.*?\n(#.*\n)*#.*(\n)*/, '')
    content.sub!(/^[\n\s]*/, '')

    # Write it back
    File.open(file_name, "w") do |f|
      f.print "# -*- #{encoding_comment.strip} -*-\n\n" unless encoding_comment.blank?
      if self.bottom?
        f.print content
        f.print "\n\n"
        f.print info_block
      else
        f.print info_block
        f.print "\n" if self.separate?
        f.print content
      end
    end
  end
end

.append_comments(col_and_lines) ⇒ Object



123
124
125
126
127
128
129
130
131
132
# File 'lib/annotate_models.rb', line 123

def self.append_comments(col_and_lines)
  max_length = col_and_lines.map{|(col, line)| line.length}.max
  col_and_lines.map do |(col, line)|
    if col.comment.blank?
      line
    else
      "%-#{max_length}s # %s" % [line, col.comment]
    end
  end
end

.bottom?Boolean

Returns:

  • (Boolean)


44
45
46
# File 'lib/annotate_models.rb', line 44

def self.bottom?
  self.position =~ /bottom/i
end

.do_annotationsObject

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



210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
# File 'lib/annotate_models.rb', line 210

def self.do_annotations
  annotated = self.get_model_names.inject([]) do |list, class_name|
    begin
      # klass = class_name.split('::').inject(Object){ |klass,part| klass.const_get(part) }
      klass = class_name.constantize
      if klass < ActiveRecord::Base && !klass.abstract_class?
        list << class_name
        self.annotate(klass)
      end
    rescue Exception => e
      puts "Unable to annotate #{class_name}: #{e.message}"
    end
    list
  end
  puts "Annotated #{annotated.join(', ')}"
end

.get_model_namesObject

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.



192
193
194
195
196
197
198
199
200
201
202
203
204
# File 'lib/annotate_models.rb', line 192

def self.get_model_names
  result = nil
  if self.models.empty?
    Dir.chdir(self.model_dir) do
      result = Dir["**/*.rb"].map do |filename|
        filename.sub(/\.rb$/, '').camelize
      end
    end
  else
    result = self.models.dup
  end
  result
end

.get_schema_info(klass) ⇒ Object

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



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
# File 'lib/annotate_models.rb', line 71

def self.get_schema_info(klass)
  table_info = "# Table name: #{klass.table_name}"
  table_info << " # #{klass.table_comment}" unless klass.table_comment.blank?
  table_info << "\n#\n"
  max_size = klass.column_names.collect{|name| name.size}.max + 1

  columns = klass.columns

  cols = if self.sort_columns
           pk    = columns.find_all { |col| col.name == klass.primary_key }.flatten
           assoc = columns.find_all { |col| col.name.match(/_id$/) }.sort_by(&:name)
           dates = columns.find_all { |col| col.name.match(/_on$/) }.sort_by(&:name)
           times = columns.find_all { |col| col.name.match(/_at$/) }.sort_by(&:name)
           pk + assoc + (columns - pk - assoc - times - dates).compact.sort_by(&:name) + dates + times
         else
           columns
         end

  col_lines = append_comments(cols.map{|col| [col, annotate_column(col, klass, max_size)]})
  cols_text = col_lines.join("\n")

  result = "# #{self.output_prefix}\n# \n# Schema version: #{get_schema_version}\n#\n"
  result << table_info
  result << cols_text
  result << "\n# \n# #{self.output_suffix}" unless self.output_suffix.blank?
  result << "\n"
  result
end

.get_schema_versionObject



227
228
229
230
231
232
233
# File 'lib/annotate_models.rb', line 227

def self.get_schema_version
  unless @schema_version
    version = ActiveRecord::Migrator.current_version rescue 0
    @schema_version = version > 0 ? version : ''
  end
  @schema_version
end

.modelsObject



28
29
30
# File 'lib/annotate_models.rb', line 28

def self.models
  (ENV['MODELS'] || '').split(',')
end

.output_prefixObject



36
37
38
# File 'lib/annotate_models.rb', line 36

def self.output_prefix
  bottom? ? PREFIX_AT_BOTTOM : PREFIX_ON_TOP
end

.output_suffixObject



40
41
42
# File 'lib/annotate_models.rb', line 40

def self.output_suffix
  bottom? ? SUFFIX_AT_BOTTOM : SUFFIX_ON_TOP
end

.quote(value) ⇒ Object

Simple quoting for the default column value



54
55
56
57
58
59
60
61
62
63
64
65
# File 'lib/annotate_models.rb', line 54

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.inspect
    # BigDecimals need to be output in a non-normalized form and quoted.
  when BigDecimal               then value.to_s('F')
  else
    value.inspect
  end
end

.separate?Boolean

Returns:

  • (Boolean)


48
49
50
# File 'lib/annotate_models.rb', line 48

def self.separate?
  ENV['SEPARATE'] =~ /yes|on|true/i
end

.sort_columnsObject



32
33
34
# File 'lib/annotate_models.rb', line 32

def self.sort_columns
  ENV['SORT'] =~ /yes|on|true/i
end