lib/annotate/annotate_models.rb in annotate-2.4.1.beta1 vs lib/annotate/annotate_models.rb in annotate-2.5.0.pre1
- old
+ new
@@ -1,22 +1,43 @@
module AnnotateModels
- class << self
- # Annotate Models plugin use this header
- COMPAT_PREFIX = "== Schema Info"
- PREFIX = "== Schema Information"
+ # Annotate Models plugin use this header
+ COMPAT_PREFIX = "== Schema Info"
+ COMPAT_PREFIX_MD = "## Schema Info"
+ PREFIX = "== Schema Information"
+ PREFIX_MD = "## Schema Information"
+ END_MARK = "== Schema Information End"
+ PATTERN = /^\n?# (?:#{COMPAT_PREFIX}|#{COMPAT_PREFIX_MD}).*?\n(#.*\n)*\n/
- FIXTURE_DIRS = ["test/fixtures","spec/fixtures"]
- # File.join for windows reverse bar compat?
- # I dont use windows, can`t test
- UNIT_TEST_DIR = File.join("test", "unit" )
- SPEC_MODEL_DIR = File.join("spec", "models")
- # Object Daddy http://github.com/flogic/object_daddy/tree/master
- EXEMPLARS_TEST_DIR = File.join("test", "exemplars")
- EXEMPLARS_SPEC_DIR = File.join("spec", "exemplars")
- # Machinist http://github.com/notahat/machinist
- BLUEPRINTS_DIR = File.join("test", "blueprints")
+ # File.join for windows reverse bar compat?
+ # I dont use windows, can`t test
+ UNIT_TEST_DIR = File.join("test", "unit" )
+ SPEC_MODEL_DIR = File.join("spec", "models")
+ FIXTURE_TEST_DIR = File.join("test", "fixtures")
+ FIXTURE_SPEC_DIR = File.join("spec", "fixtures")
+ FIXTURE_DIRS = ["test/fixtures","spec/fixtures"]
+ # Object Daddy http://github.com/flogic/object_daddy/tree/master
+ EXEMPLARS_TEST_DIR = File.join("test", "exemplars")
+ EXEMPLARS_SPEC_DIR = File.join("spec", "exemplars")
+
+ # Machinist http://github.com/notahat/machinist
+ BLUEPRINTS_TEST_DIR = File.join("test", "blueprints")
+ BLUEPRINTS_SPEC_DIR = File.join("spec", "blueprints")
+
+ # Factory Girl http://github.com/thoughtbot/factory_girl
+ FACTORY_GIRL_TEST_DIR = File.join("test", "factories")
+ FACTORY_GIRL_SPEC_DIR = File.join("spec", "factories")
+
+ # Fabrication https://github.com/paulelliott/fabrication.git
+ FABRICATORS_TEST_DIR = File.join("test", "fabricators")
+ FABRICATORS_SPEC_DIR = File.join("spec", "fabricators")
+
+ # Don't show limit (#) on these column types
+ # Example: show "integer" instead of "integer(4)"
+ NO_LIMIT_COL_TYPES = ["integer", "boolean"]
+
+ class << self
def model_dir
@model_dir || "app/models"
end
def model_dir=(dir)
@@ -24,79 +45,104 @@
end
# Simple quoting for the default column value
def quote(value)
case value
- when NilClass then "NULL"
- when TrueClass then "TRUE"
- when FalseClass then "FALSE"
- when Float, Fixnum, Bignum then value.to_s
+ 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
+ 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 get_schema_info(klass, header, options = {})
- info = "# #{header}\n#\n"
- info << "# Table name: #{klass.table_name}\n#\n"
+ info = "# #{header}\n"
+ info<< "#\n"
+ info<< "# Table name: #{klass.table_name}\n"
+ info<< "#\n"
- max_size = klass.column_names.collect{|name| name.size}.max + 1
- klass.columns.each do |col|
+ max_size = klass.column_names.map{|name| name.size}.max || 0
+ max_size += options[:format_rdoc] ? 5 : 1
+
+ if(options[:format_markdown])
+ info<< sprintf( "# %-#{max_size + 4}.#{max_size + 4}s | %-18.18s | %s\n", 'Field', 'Type', 'Attributes' )
+ info<< "# #{ '-' * ( max_size + 4 ) } | #{'-' * 18} | #{ '-' * 25 }\n"
+ end
+
+ cols = klass.columns
+ cols = cols.sort_by(&:name) unless(options[:no_sort])
+ cols.each do |col|
attrs = []
attrs << "default(#{quote(col.default)})" unless col.default.nil?
attrs << "not null" unless col.null
- attrs << "primary key" if col.name == klass.primary_key
+ attrs << "primary key" if col.name.to_sym == klass.primary_key.to_sym
- col_type = col.type.to_s
+ col_type = (col.type || col.sql_type).to_s
if col_type == "decimal"
col_type << "(#{col.precision}, #{col.scale})"
else
- col_type << "(#{col.limit})" if col.limit
+ if (col.limit)
+ col_type << "(#{col.limit})" unless NO_LIMIT_COL_TYPES.include?(col_type)
+ end
end
# Check out if we got a geometric column
# and print the type and SRID
if col.respond_to?(:geometry_type)
attrs << "#{col.geometry_type}, #{col.srid}"
end
# Check if the column has indices and print "indexed" if true
- # If the indice include another colum, print it too.
- if options[:simple_indexes] # Check out if this column is indexed
+ # If the index includes another column, print it too.
+ if options[:simple_indexes] && klass.table_exists?# Check out if this column is indexed
indices = klass.connection.indexes(klass.table_name)
if indices = indices.select { |ind| ind.columns.include? col.name }
indices.each do |ind|
ind = ind.columns.reject! { |i| i == col.name }
attrs << (ind.length == 0 ? "indexed" : "indexed => [#{ind.join(", ")}]")
end
end
end
- info << sprintf("# %-#{max_size}.#{max_size}s:%-15.15s %s", col.name, col_type, attrs.join(", ")).rstrip + "\n"
+ if options[:format_rdoc]
+ info << sprintf("# %-#{max_size}.#{max_size}s<tt>%s</tt>", "*#{col.name}*::", attrs.unshift(col_type).join(", ")).rstrip + "\n"
+ elsif options[:format_markdown]
+ info << sprintf("# **%-#{max_size}.#{max_size}s** | `%-16.16s` | `%s`", col.name, col_type, attrs.join(", ").rstrip) + "\n"
+ else
+ info << sprintf("# %-#{max_size}.#{max_size}s:%-16.16s %s", col.name, col_type, attrs.join(", ")).rstrip + "\n"
+ end
end
- if options[:show_indexes]
+ if options[:show_indexes] && klass.table_exists?
info << get_index_info(klass)
end
- info << "#\n\n"
+ if options[:format_rdoc]
+ info << "#--\n"
+ info << "# #{END_MARK}\n"
+ info << "#++\n\n"
+ else
+ info << "#\n\n"
+ end
end
def get_index_info(klass)
index_info = "#\n# Indexes\n#\n"
indexes = klass.connection.indexes(klass.table_name)
return "" if indexes.empty?
max_size = indexes.collect{|index| index.name.size}.max + 1
- indexes.each do |index|
+ indexes.sort_by{|index| index.name}.each do |index|
index_info << sprintf("# %-#{max_size}.#{max_size}s %s %s", index.name, "(#{index.columns.join(",")})", index.unique ? "UNIQUE" : "").rstrip + "\n"
end
return index_info
end
@@ -106,51 +152,69 @@
# info block and write a new one.
# Returns true or false depending on whether the file was modified.
#
# === Options (opts)
# :position<Symbol>:: where to place the annotated section in fixture or model file,
- # "before" or "after". Default is "before".
+ # :before or :after. Default is :before.
# :position_in_class<Symbol>:: where to place the annotated section in model file
# :position_in_fixture<Symbol>:: where to place the annotated section in fixture file
# :position_in_others<Symbol>:: where to place the annotated section in the rest of
# supported files
#
def annotate_one_file(file_name, info_block, options={})
if File.exist?(file_name)
old_content = File.read(file_name)
+ return false if(old_content =~ /# -\*- SkipSchemaAnnotations.*\n/)
# Ignore the Schema version line because it changes with each migration
- header = Regexp.new(/(^# Table name:.*?\n(#.*[\r]?\n)*[\r]?\n)/)
- old_header = old_content.match(header).to_s
- new_header = info_block.match(header).to_s
+ header_pattern = /(^# Table name:.*?\n(#.*[\r]?\n)*[\r]?\n)/
+ old_header = old_content.match(header_pattern).to_s
+ new_header = info_block.match(header_pattern).to_s
- old_columns = old_header && old_header.scan(/#[\t\s]+([\w\d]+)[\t\s]+\:([\d\w]+)/).sort
- new_columns = new_header && new_header.scan(/#[\t\s]+([\w\d]+)[\t\s]+\:([\d\w]+)/).sort
+ column_pattern = /^#[\t ]+\w+[\t ]+.+$/
+ old_columns = old_header && old_header.scan(column_pattern).sort
+ new_columns = new_header && new_header.scan(column_pattern).sort
- if old_columns == new_columns
+ encoding = Regexp.new(/(^#\s*encoding:.*\n)|(^# coding:.*\n)|(^# -\*- coding:.*\n)/)
+ encoding_header = old_content.match(encoding).to_s
+
+ if old_columns == new_columns && !options[:force]
false
else
- # Replace the old schema info with the new schema info
- new_content = old_content.sub(/^# #{COMPAT_PREFIX}.*?\n(#.*\n)*\n/, info_block)
- # But, if there *was* no old schema info, we simply need to insert it
- if new_content == old_content
- new_content = options[:position] == 'before' ?
- (info_block + old_content) :
- ((old_content =~ /\n$/ ? old_content : old_content + '\n') + info_block)
- end
+
+# todo: figure out if we need to extract any logic from this merge chunk
+# <<<<<<< HEAD
+# # Replace the old schema info with the new schema info
+# new_content = old_content.sub(/^# #{COMPAT_PREFIX}.*?\n(#.*\n)*\n*/, info_block)
+# # But, if there *was* no old schema info, we simply need to insert it
+# if new_content == old_content
+# old_content.sub!(encoding, '')
+# new_content = options[:position] == 'after' ?
+# (encoding_header + (old_content =~ /\n$/ ? old_content : old_content + "\n") + info_block) :
+# (encoding_header + info_block + old_content)
+# end
+# =======
+ # Strip the old schema info, and insert new schema info.
+ old_content.sub!(encoding, '')
+ old_content.sub!(PATTERN, '')
+
+ new_content = (options[:position] || 'before').to_s == 'after' ?
+ (encoding_header + (old_content.rstrip + "\n\n" + info_block)) :
+ (encoding_header + info_block + old_content)
+
File.open(file_name, "wb") { |f| f.puts new_content }
true
end
end
end
def remove_annotation_of_file(file_name)
if File.exist?(file_name)
content = File.read(file_name)
- content.sub!(/^# #{COMPAT_PREFIX}.*?\n(#.*\n)*\n/, '')
+ content.sub!(PATTERN, '')
File.open(file_name, "wb") { |f| f.puts content }
end
end
@@ -158,68 +222,80 @@
# 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.
# Returns true or false depending on whether the source
# files were modified.
- def annotate(klass, file, header,options={})
+ def annotate(klass, file, header, options={})
info = get_schema_info(klass, header, options)
annotated = false
model_name = klass.name.underscore
model_file_name = File.join(model_dir, file)
if annotate_one_file(model_file_name, info, options_with_position(options, :position_in_class))
annotated = true
end
-
- unless ENV['exclude_tests']
+
+ unless options[:exclude_tests]
[
- File.join(UNIT_TEST_DIR, "#{model_name}_test.rb"), # test
- File.join(SPEC_MODEL_DIR, "#{model_name}_spec.rb"), # spec
- ].each do |file|
+ find_test_file(UNIT_TEST_DIR, "#{model_name}_test.rb"), # test
+ find_test_file(SPEC_MODEL_DIR, "#{model_name}_spec.rb"), # spec
+ ].each do |file|
# todo: add an option "position_in_test" -- or maybe just ask if anyone ever wants different positions for model vs. test vs. fixture
- annotate_one_file(file, info, options_with_position(options, :position_in_fixture))
+ if annotate_one_file(file, info, options_with_position(options, :position_in_fixture))
+ annotated = true
+ end
end
end
- unless ENV['exclude_fixtures']
+ unless options[:exclude_fixtures]
[
- File.join(EXEMPLARS_TEST_DIR, "#{model_name}_exemplar.rb"), # Object Daddy
- File.join(EXEMPLARS_SPEC_DIR, "#{model_name}_exemplar.rb"), # Object Daddy
- File.join(BLUEPRINTS_DIR, "#{model_name}_blueprint.rb"), # Machinist Blueprints
- ].each do |file|
- annotate_one_file(file, info, options_with_position(options, :position_in_fixture))
- end
-
- FIXTURE_DIRS.each do |dir|
- fixture_file_name = File.join(dir,klass.table_name + ".yml")
- if File.exist?(fixture_file_name)
- annotate_one_file(fixture_file_name, info, options_with_position(options, :position_in_fixture))
+ File.join(FIXTURE_TEST_DIR, "#{klass.table_name}.yml"), # fixture
+ File.join(FIXTURE_SPEC_DIR, "#{klass.table_name}.yml"), # fixture
+ File.join(EXEMPLARS_TEST_DIR, "#{model_name}_exemplar.rb"), # Object Daddy
+ File.join(EXEMPLARS_SPEC_DIR, "#{model_name}_exemplar.rb"), # Object Daddy
+ File.join(BLUEPRINTS_TEST_DIR, "#{model_name}_blueprint.rb"), # Machinist Blueprints
+ File.join(BLUEPRINTS_SPEC_DIR, "#{model_name}_blueprint.rb"), # Machinist Blueprints
+ File.join(FACTORY_GIRL_TEST_DIR, "#{model_name}_factory.rb"), # Factory Girl Factories
+ File.join(FACTORY_GIRL_SPEC_DIR, "#{model_name}_factory.rb"), # Factory Girl Factories
+ File.join(FABRICATORS_TEST_DIR, "#{model_name}_fabricator.rb"), # Fabrication Fabricators
+ File.join(FABRICATORS_SPEC_DIR, "#{model_name}_fabricator.rb"), # Fabrication Fabricators
+ ].each do |file|
+ if annotate_one_file(file, info, options_with_position(options, :position_in_fixture))
+ annotated = true
end
end
end
-
+
annotated
end
-
+
# position = :position_in_fixture or :position_in_class
def options_with_position(options, position_in)
options.merge(:position=>(options[position_in] || options[:position]))
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
# model_dir directory.
- def get_model_files
- models = ARGV.dup
- models.shift
+ def get_model_files(options)
+ if(!options[:is_rake])
+ models = ARGV.dup
+ models.shift
+ else
+ models = []
+ end
models.reject!{|m| m.match(/^(.*)=/)}
if models.empty?
begin
Dir.chdir(model_dir) do
- models = Dir["**/*.rb"]
+ models = if options[:ignore_model_sub_dir]
+ Dir["*.rb"]
+ else
+ Dir["**/*.rb"]
+ end
end
rescue SystemCallError
puts "No models found in directory '#{model_dir}'."
puts "Either specify models on the command line, or use the --model-dir option."
puts "Call 'annotate --help' for more info."
@@ -231,24 +307,24 @@
# Retrieve the classes belonging to the model names we're asked to process
# Check for namespaced models in subdirectories as well as models
# in subdirectories without namespacing.
def get_model_class(file)
- require File.expand_path("#{model_dir}/#{file}") # this is for non-rails projects, which don't get Rails auto-require magic
- model = ActiveSupport::Inflector.camelize(file.gsub(/\.rb$/, ''))
- parts = model.split('::')
- begin
- parts.inject(Object) {|klass, part| klass.const_get(part) }
- rescue LoadError, NameError
- begin
- Object.const_get(parts.last)
- rescue LoadError, NameError
- Object.const_get(Module.constants.detect{|c|parts.last.downcase == c.downcase})
- end
- end
+ # this is for non-rails projects, which don't get Rails auto-require magic
+ require File.expand_path("#{model_dir}/#{file}")
+
+ model_path = file.gsub(/\.rb$/, '')
+ get_loaded_model(model_path) || get_loaded_model(model_path.split('/').last)
end
+ # Retrieve loaded model class by path to the file where it's supposed to be defined.
+ def get_loaded_model(model_path)
+ ObjectSpace.each_object.
+ select { |c| c.is_a?(Class) && c.ancestors.include?(ActiveRecord::Base) }.
+ detect { |c| ActiveSupport::Inflector.underscore(c) == model_path }
+ 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 pass it to the associated block
def do_annotations(options={})
@@ -256,11 +332,11 @@
options[:require].each do |path|
require path
end
end
- header = PREFIX.dup
+ header = options[:format_markdown] ? PREFIX_MD.dup : PREFIX.dup
if options[:include_version]
version = ActiveRecord::Migrator.current_version rescue 0
if version > 0
header << "\n# Schema version: #{version}"
@@ -270,23 +346,21 @@
if options[:model_dir]
self.model_dir = options[:model_dir]
end
annotated = []
- get_model_files.each do |file|
+ get_model_files(options).each do |file|
begin
klass = get_model_class(file)
- if klass < ActiveRecord::Base && !klass.abstract_class?
+ if klass && klass < ActiveRecord::Base && !klass.abstract_class?
if annotate(klass, file, header, options)
annotated << klass
end
end
rescue Exception => e
- puts "Unable to annotate #{file}: #{e.inspect}"
- puts ""
-# todo: check if all backtrace lines are in "gems" -- if so, it's an annotate bug, so print the whole stack trace.
-# puts e.backtrace.join("\n\t")
+ # todo: check if all backtrace lines are in "gems" -- if so, it's an annotate bug, so print the whole stack trace.
+ puts "Unable to annotate #{file}: #{e.message} (#{e.backtrace.first})"
end
end
if annotated.empty?
puts "Nothing annotated."
else
@@ -298,43 +372,45 @@
if options[:model_dir]
puts "removing"
self.model_dir = options[:model_dir]
end
deannotated = []
- get_model_files.each do |file|
+ get_model_files(options).each do |file|
begin
klass = get_model_class(file)
if klass < ActiveRecord::Base && !klass.abstract_class?
deannotated << klass
+ model_name = klass.name.underscore
model_file_name = File.join(model_dir, file)
remove_annotation_of_file(model_file_name)
- FIXTURE_DIRS.each do |dir|
- fixture_file_name = File.join(dir,klass.table_name + ".yml")
- remove_annotation_of_file(fixture_file_name) if File.exist?(fixture_file_name)
- end
-
- [ File.join(UNIT_TEST_DIR, "#{klass.name.underscore}_test.rb"),
- File.join(SPEC_MODEL_DIR,"#{klass.name.underscore}_spec.rb")].each do |file|
+ [
+ File.join(UNIT_TEST_DIR, "#{model_name}_test.rb"),
+ File.join(SPEC_MODEL_DIR, "#{model_name}_spec.rb"),
+ File.join(FIXTURE_TEST_DIR, "#{klass.table_name}.yml"), # fixture
+ File.join(FIXTURE_SPEC_DIR, "#{klass.table_name}.yml"), # fixture
+ File.join(EXEMPLARS_TEST_DIR, "#{model_name}_exemplar.rb"), # Object Daddy
+ File.join(EXEMPLARS_SPEC_DIR, "#{model_name}_exemplar.rb"), # Object Daddy
+ File.join(BLUEPRINTS_TEST_DIR, "#{model_name}_blueprint.rb"), # Machinist Blueprints
+ File.join(BLUEPRINTS_SPEC_DIR, "#{model_name}_blueprint.rb"), # Machinist Blueprints
+ File.join(FACTORY_GIRL_TEST_DIR, "#{model_name}_factory.rb"), # Factory Girl Factories
+ File.join(FACTORY_GIRL_SPEC_DIR, "#{model_name}_factory.rb"), # Factory Girl Factories
+ File.join(FABRICATORS_TEST_DIR, "#{model_name}_fabricator.rb"), # Fabrication Fabricators
+ File.join(FABRICATORS_SPEC_DIR, "#{model_name}_fabricator.rb"), # Fabrication Fabricators
+ ].each do |file|
remove_annotation_of_file(file) if File.exist?(file)
end
-
+
end
rescue Exception => e
puts "Unable to annotate #{file}: #{e.message}"
end
end
puts "Removed annotation from: #{deannotated.join(', ')}"
end
- end
-end
-# monkey patches
-
-module ::ActiveRecord
- class Base
- def self.method_missing(name, *args)
- # ignore this, so unknown/unloaded macros won't cause parsing to fail
+ def find_test_file(dir, file_name)
+ Dir.glob(File.join(dir, "**", file_name)).first || File.join(dir, file_name)
end
end
end