Commit 6d7bfc08 by Alex Chaffee

Merge branch 'mrjoy'

* mrjoy: (177 commits) Updating authorship. Resolve conflict between generated rakefile and CLI tool. Minor fixups. Updating readme to be RDoc compliant. fixed a few typos Updating docs to point to this fork. Update docs. Yep, I'm tired. Revert "Version bump to 0.0.0" Version bump to 0.0.0 Changing my mind on project name issue... Making project name match Github project name. Bumping version. Wider type column output, play nice with Rake CLI. Fixing behavior with some model sub-classes, adding skip-annotations feature. Making ActiveSupport dependency float, updating gemspec. Updating change log, readme and TODO list. Update generated .rake file with new options. Renaming bin to avoid conflict with ImageMagick. Accept string or symbol for positions. ... Conflicts: Gemfile Gemfile.lock History.txt README.rdoc Rakefile VERSION.yml annotate.gemspec annotate_models.gemspec lib/annotate/annotate_models.rb lib/tasks/annotate_models.rake lib/tasks/annotate_routes.rake spec/annotate/annotate_models_spec.rb spec/spec_helper.rb todo.txt
parents c52cefe2 b7735703
......@@ -6,3 +6,6 @@ spec/debug.log
pkg/*
dist
Gemfile.lock
*.gem
/.idea/
.rvmrc
rvm use ruby-1.9.2-p290@annotate_models
......@@ -3,9 +3,10 @@ source :rubygems
gem "rake"
group :development do
gem "rspec"
gem "rdoc"
gem "mg"
gem 'activesupport', '>= 2.1.0'
gem "wrong"
gem 'mg'
gem 'rspec'
gem 'wrong'
gem 'rdoc'
end
gem 'activesupport', '>= 3.0.0', :require => nil
......@@ -56,7 +56,7 @@ PLATFORMS
ruby
DEPENDENCIES
activesupport (>= 2.1.0)
activesupport (>= 3.0.0)
mg
rake
rdoc
......
......@@ -20,7 +20,53 @@
* Support FactoryGirl
* Support :change migrations (Rails 3.1)
* Allow models with non-standard capitalization
* Widen type column so we can handle longtexts with chopping things off.
* Skip trying to get list of models from commandline when running via Rake (was
preventing the use of multiple rake tasks in one command if one of them was
db:migrate).
* Add ability to skip annotations for a model by adding
'# -*- SkipSchemaAnnotations' anywhere in the file.
* Don't show column limits for integer and boolean types.
* Add sorting for columns and indexes. (Helpful for out-of-order migration
execution, but use --no-sort if you don't want this.)
* Annotate unit tests in subfolders.
* Add generator to install rakefile that automatically annotates on db:migrate.
* Correct Gemfile to clarify which environments need which gems.
* Add an .rvmrc to facilitate clean development.
* Refactor out ActiveRecord monkey-patch to permit extending without
side-effects.
* Use ObjectSpace to locate models to facilitate handling of models with
non-standard capitalization.
Note that this still requires that the inflector be configured to understand
the special case.
* Shore up test cases a bit.
* Merge against many of the older branches on Github whose functionality is
already reflected to reduce confusion about what is and is not implemented
here.
* Accept String or Symbol for :position (et al) options.
* Rename "annotate" bin to "annotate_models" to avoid conflicting with
ImageMagick.
* Add RDoc output formatting as an option.
* Add Markdown output formatting as an option.
* Add option to force annotation regeneration.
* Add new configuration option for controlling where info is placed in
fixtures/factories.
* Fix for models without tables.
* Fix gemspec generation now that Jeweler looks at Gemfile.
* Fix warning: `NOTE: Gem::Specification#default_executable= is deprecated with
no replacement. It will be removed on or after 2011-10-01.`
* Fix handling of files with no trailing newline when putting annotations at
the end of the file.
== 2.4.2 2009-11-21
* Annotates (spec|test)/factories/<model>_factory.rb files
== 2.4.1 2009-11-20
* Annotates thoughtbot's factory_girl factories (test/factories/<model>_factory.rb)
* Move default annotation position back to top
>>>>>>> mrjoy
== 2.4.0 2009-12-13
......
......@@ -8,6 +8,7 @@ Add a comment summarizing the current schema to the top or bottom of each of you
* Object Daddy exemplars
* Machinist blueprints
* Fabrication fabricators
* Thoughtbot's factory_girl factories, i.e. the (spec|test)/factories/<model>_factory.rb files
The schema comment looks like this:
......@@ -41,7 +42,7 @@ Also, if you pass the -r option, it'll annotate routes.rb with the output of "ra
Into Gemfile from Github:
gem 'annotate', :git => 'git://github.com/ctran/annotate_models.git'
gem 'annotate', :git => 'git://github.com/MrJoy/annotate_models.git'
Into environment gems From rubygems.org:
......@@ -49,8 +50,8 @@ Into environment gems From rubygems.org:
Into environment gems from Github checkout:
git clone git://github.com/ctran/annotate_models.git annotate
cd annotate
git clone git://github.com/MrJoy/annotate_models.git annotate_models
cd annotate_models
rake build
gem install pkg/annotate-*.gem
......@@ -61,60 +62,61 @@ Into environment gems from Github checkout:
To annotate all your models, tests, fixtures, etc.:
cd /path/to/app
annotate
annotate_models
To annotate your models and tests:
annotate --exclude fixtures
annotate_models --exclude fixtures
To annotate just your models:
annotate --exclude tests,fixtures
annotate_models --exclude tests,fixtures
To annotate routes.rb:
annotate -r
annotate_models -r
To automatically annotate after running 'rake db:migrate':
To remove annotations:
[*needs more clarity*] unpack the gem into vendor/plugins, or maybe vendor/gems, or maybe just require tasks/migrate.rake.
annotate_models -d
If you install annotate_models as a plugin, it will automatically
adjust your <tt>rake db:migrate</tt> tasks so that they update the
annotations in your model files for you once the migration is
completed. To get the same behavior from a gem, add the following to
your Rakefile:
To automatically annotate after running 'rake db:migrate', ensure you've added
annotate_models to your Rails project's Gemfile, and run this:
require 'annotate/tasks'
rails g annotate_models:install
To customize the behavior of annotate when it is running as a Rake
task, use the following (in your Rakefile or wherever):
This will produce a .rake file that will ensure annotation happens after
migration (but only in development mode), and provide configuration options
you can use to tailor the output.
ENV['position_in_class'] = "before"
ENV['position_in_fixture'] = "before"
ENV['show_indexes'] = "false"
ENV['include_version'] = "false"
ENV['exclude_tests'] = "false"
ENV['exclude_fixtures'] = "false"
ENV['skip_on_db_migrate'] = "false"
Warning: ImageMagick installs a tool called `annotate` too (if you're using MacPorts it's in `/opt/local/bin/annotate`. So if you see Usage: annotate imagein.jpg imageout.jpg then put `/usr/bin` ahead on the path and you'll get ours instead.
If you want to always skip annotations on a particular model, add this string
anywhere in the file:
# -*- SkipSchemaAnnotations
== OPTIONS
Usage: annotate [options] [model_file]*
Usage: annotate_models [options] [model_file]*
-d, --delete Remove annotations from all model files
-p, --position [before|after] Place the annotations at the top (before) or the bottom (after) of the model file
-r, --routes Annotate routes.rb with the output of 'rake routes'
-v, --version Show the current version of this gem
-m, --show-migration Include the migration version number in the annotation
-i, --show-indexes List the table's database indexes in the annotation
-s, --simple-indexes Concat the column's related indexes in the annotation
-i, --show-indexes List the indexes for the table in the annotation
-s, --simple-indexes Include information about indexes inline with the relevant column
--model-dir dir Annotate model files stored in dir rather than app/models
--ignore-model-subdirs Ignore sub-directories of the models directory.
-R, --require path Additional files to require before loading models
-e, --exclude [tests,fixtures] Do not annotate fixtures, test files, or both
-e [tests,fixtures] Skip annotation of fixtures/factories/test files
--exclude
-n --no-sort Sort by column creation order rather than alphabetical order
== SORTING
By default, columns will be sorted alphabetically so that the results of
annotation are consistent regardless of what order migrations are executed in.
If you prefer the old behavior, use --no-sort.
== WARNING
......@@ -123,16 +125,16 @@ block in your models if it looks like it was previously added
by annotate models, so you don't want to add additional text
to an automatically created comment block.
* * Back up your model files before using... * *
BACK UP YOUR MODELS BEFORE USING THIS TOOL!
== LINKS
* Factory Girl => http://github.com/thoughtbot/factory_girl (NOT IMPLEMENTED)
* Object Daddy => http://github.com/flogic/object_daddy
* Machinist => http://github.com/notahat/machinist
* Fabrication => http://github.com/paulelliott/fabrication
* SpatialAdapter => http://github.com/pdeffendol/spatial_adapter
* PostgisAdapter => http://github.com/nofxx/postgis_adapter
- Factory Girl: http://github.com/thoughtbot/factory_girl
- Object Daddy: http://github.com/flogic/object_daddy
- Machinist: http://github.com/notahat/machinist
- Fabrication: http://github.com/paulelliott/fabrication
- SpatialAdapter: http://github.com/pdeffendol/spatial_adapter
- PostgisAdapter: http://github.com/nofxx/postgis_adapter
== LICENSE:
......@@ -150,21 +152,23 @@ Maintained by: Alex Chaffee and Cuong Tran
Homepage: http://github.com/ctran/annotate_models
Modifications by:
- Alex Chaffee - http://github.com/alexch - alex@pivotallabs.com
- Cuong Tran - http://github.com/ctran - ctran@pragmaquest.com
- Jack Danger - http://github.com/JackDanger
- Michael Bumann - http://github.com/bumi
- Henrik Nyh - http://github.com/henrik
- Marcos Piccinini - http://github.com/nofxx
- Neal Clark - http://github.com/nclark
- Jacqui Maher - http://github.com/jacqui
- Nick Plante - http://github.com/zapnap - http://blog.zerosum.org
- Pedro Visintin - http://github.com/peterpunk - http://www.pedrovisintin.com
- Bob Potter - http://github.com/bpot
- Gavin Montague - http://github.com/govan/
- Alexander Semyonov - http://github.com/rotuka/
- Ian Duggan http://github.com/ijcd/
With help from:
- Alex Chaffee - http://github.com/alexch - alex@pivotallabs.com
- Cuong Tran - http://github.com/ctran - ctran@pragmaquest.com
- Jack Danger - http://github.com/JackDanger
- Michael Bumann - http://github.com/bumi
- Henrik Nyh - http://github.com/henrik
- Marcos Piccinini - http://github.com/nofxx
- Neal Clark - http://github.com/nclark
- Jacqui Maher - http://github.com/jacqui
- Nick Plante - http://github.com/zapnap - http://blog.zerosum.org
- Pedro Visintin - http://github.com/peterpunk - http://www.pedrovisintin.com
- Bob Potter - http://github.com/bpot
- Gavin Montague - http://github.com/govan/
- Alexander Semyonov - http://github.com/rotuka/
- Nathan Brazil - http://github.com/bitaxis/
- Ian Duggan http://github.com/ijcd/
- Jon Frisby http://github.com/mrjoy/
and many others that I may have missed to add.
here = File.dirname __FILE__
require 'rubygems'
require 'bundler'
begin
Bundler.setup(:default, :development)
rescue Bundler::BundlerError => e
$stderr.puts e.message
$stderr.puts "Run `bundle install` to install missing gems"
exit e.status_code
end
require 'rake/dsl_definition'
require 'rake'
require "#{here}/lib/annotate"
# want other tests/tasks run by default? Add them to the list
......@@ -25,12 +35,7 @@ begin
rescue LoadError
abort "Please `gem install mg`"
end
MG.new("annotate_models.gemspec")
require 'rspec/core/rake_task'
RSpec::Core::RakeTask.new(:spec) do |spec|
spec.pattern = 'spec/**/*_spec.rb'
end
MG.new("annotate.gemspec")
task :default => :spec
......@@ -39,18 +44,10 @@ RSpec::Core::RakeTask.new(:spec) do |t|
t.pattern = ['spec/*_spec.rb', 'spec/**/*_spec.rb']
end
# FIXME not working yet
RSpec::Core::RakeTask.new(:rcov) do |t|
t.pattern = 'spec/**/*_spec.rb'
t.rcov = true
require 'rdoc/task'
RDoc::Task.new do |rdoc|
rdoc.rdoc_dir = 'rdoc'
rdoc.title = "annotated_models #{Annotate.version}"
rdoc.rdoc_files.include('README*')
rdoc.rdoc_files.include('lib/**/*.rb')
end
# FIXME warns "already initialized constant Task"
# FIXME throws "uninitialized constant RDoc::VISIBILITIES"
# require 'rdoc/task'
# RDoc::Task.new do |rdoc|
# rdoc.main = "README.rdoc"
# rdoc.rdoc_files.include("README.rdoc", "lib/**/*.rb")
# # require 'lib/annotate'
# # rdoc.title = "annotate #{Annotate.version}"
# end
require './lib/annotate'
Gem::Specification.new do |s|
s.name = %q{annotate_models}
s.version = Annotate.version
s.description = %q{Annotates Rails/ActiveRecord Models, routes, fixtures, and others based on the database schema.}
s.summary = %q{Annotates Rails Models, routes, fixtures, and others based on the database schema.}
s.authors = ["Cuong Tran", "Alex Chaffee", "Marcos Piccinini", "Turadg Aleahmad"]
s.email = ["alex@stinky.com", "ctran@pragmaquest.com", "x@nofxx.com", "turadg@aleahmad.net"]
s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
s.extra_rdoc_files = ["README.rdoc"]
s.files = %w( README.rdoc VERSION.yml History.txt )
s.files += Dir.glob("lib/**/*")
s.files += Dir.glob("tasks/**/*")
s.files += ["bin/annotate"] # todo: annotate_models
s.homepage = %q{http://github.com/ctran/annotate_models}
s.rdoc_options = ["--charset=UTF-8"]
s.require_paths = ["lib"]
s.rubyforge_project = %q{annotate}
s.add_runtime_dependency 'rake' # ?
end
......@@ -2,11 +2,13 @@
require 'optparse'
require 'annotate'
require 'rake/dsl_definition'
require 'rake'
task = :annotate_models
OptionParser.new do |opts|
opts.banner = "Usage: annotate [options] [model_file]*"
opts.banner = "Usage: annotate_models [options] [model_file]*"
opts.on('-d', '--delete',
"Remove annotations from all model files") do
......@@ -48,6 +50,16 @@ OptionParser.new do |opts|
ENV['model_dir'] = dir
end
opts.on('--ignore-model-subdirects',
"Ignore subdirectories of the models directory") do |dir|
ENV['ignore_model_sub_dir'] = "yes"
end
opts.on('-n', '--no-sort',
"Sort columns in creation order rather than alphabetically") do |dir|
ENV['no_sort'] = "yes"
end
opts.on('-R', '--require path',
"Additional files to require before loading models") do |path|
if ENV['require']
......@@ -61,8 +73,17 @@ OptionParser.new do |opts|
exclusions.each { |exclusion| ENV["exclude_#{exclusion}"] = "yes" }
end
opts.on('-f', '--format [bare|rdoc|markdown]', ['bare', 'rdoc', 'markdown'], 'Render Schema Infomation as plain/RDoc/Markdown') do |fmt|
ENV["format_#{fmt}"] = 'yes'
end
opts.on('--force', 'Force new annotations even if there are no changes.') do |force|
ENV['force'] = 'yes'
end
end.parse!
ENV['is_cli'] = '1'
if Annotate.load_tasks
Rake::Task[task].invoke
else
......
$:.unshift(File.dirname(__FILE__)) unless
$:.include?(File.dirname(__FILE__)) || $:.include?(File.expand_path(File.dirname(__FILE__)))
require 'yaml'
......@@ -18,6 +16,8 @@ module Annotate
if File.exists?('Rakefile')
require 'rake'
load 'Rakefile'
# Rails 3 wants to load our .rake files for us.
# TODO: selectively do this require on Rails 2.x?
Dir[File.join(File.dirname(__FILE__), 'tasks', '**/*.rake')].each { |rake| load rake }
return true
else
......
# monkey patches
module ::ActiveRecord
class Base
def self.method_missing(name, *args)
# ignore this, so unknown/unloaded macros won't cause parsing to fail
end
end
end
\ No newline at end of file
module AnnotateModels
class << self
# 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
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_DIR = File.join("test", "blueprints")
# FactoryGirl http://github.com/thoughtbot/factory_girl
FACTORIES_TEST_DIR = File.join("test", "factories")
FACTORIES_SPEC_DIR = File.join("spec", "factories")
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
......@@ -49,21 +63,34 @@ module AnnotateModels
# 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.map{|name| name.size}.max || 0
max_size += options[:format_rdoc] ? 5 : 1
max_size = klass.column_names.collect{|name| name.size}.max + 1
klass.columns.each do |col|
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
......@@ -73,8 +100,8 @@ module AnnotateModels
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|
......@@ -84,15 +111,27 @@ module AnnotateModels
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
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"
......@@ -100,7 +139,8 @@ module AnnotateModels
indexes = klass.connection.indexes(klass.table_name)
return "" if indexes.empty?
max_size = indexes.collect{|index| index.name.size}.max + 1
max_size = indexes.collect{|index| index.name.size}.max || 0
max_size += 1
indexes.each do |index|
index_info << sprintf("# %-#{max_size}.#{max_size}s %s %s", index.name, "(#{index.columns.join(",")})", index.unique ? "UNIQUE" : "").rstrip + "\n"
end
......@@ -115,7 +155,7 @@ module AnnotateModels
#
# === 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
......@@ -124,6 +164,7 @@ module AnnotateModels
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_pattern = /(^# Table name:.*?\n(#.*[\r]?\n)*[\r]?\n)/
......@@ -134,21 +175,32 @@ module AnnotateModels
old_columns = old_header && old_header.scan(column_pattern).sort
new_columns = new_header && new_header.scan(column_pattern).sort
encoding = Regexp.new(/(^# encoding:.*\n)|(^# coding:.*\n)|(^# -\*- coding:.*\n)/)
encoding = Regexp.new(/(^#\s*encoding:.*\n)|(^# coding:.*\n)|(^# -\*- coding:.*\n)/)
encoding_header = old_content.match(encoding).to_s
if old_columns == new_columns
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
# 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, '')
new_content = options[:position] == 'after' ?
(encoding_header + (old_content =~ /\n$/ ? old_content : old_content + "\n") + info_block) :
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)
end
File.open(file_name, "wb") { |f| f.puts new_content }
true
......@@ -160,7 +212,7 @@ module AnnotateModels
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
......@@ -184,8 +236,8 @@ module AnnotateModels
unless options[:exclude_tests]
[
File.join(UNIT_TEST_DIR, "#{model_name}_test.rb"), # test
File.join(SPEC_MODEL_DIR, "#{model_name}_spec.rb"), # spec
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
if annotate_one_file(file, info, options_with_position(options, :position_in_fixture))
......@@ -196,11 +248,14 @@ module AnnotateModels
unless options[:exclude_fixtures]
[
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_DIR, "#{model_name}_blueprint.rb"), # Machinist Blueprints
File.join(FACTORIES_TEST_DIR, "#{model_name.pluralize}.rb"), # FactoryGirl Factories
File.join(FACTORIES_SPEC_DIR, "#{model_name.pluralize}.rb"), # FactoryGirl Factories
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|
......@@ -208,15 +263,6 @@ module AnnotateModels
annotated = true
end
end
FIXTURE_DIRS.each do |dir|
fixture_file_name = File.join(dir,klass.table_name + ".yml")
if File.exist?(fixture_file_name)
if annotate_one_file(fixture_file_name, info, options_with_position(options, :position_in_fixture))
annotated = true
end
end
end
end
annotated
......@@ -232,14 +278,22 @@ module AnnotateModels
# the underscore or CamelCase versions of model names.
# Otherwise we take all the model files in the
# model_dir directory.
def get_model_files
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}'."
......@@ -255,21 +309,18 @@ module AnnotateModels
# Check for namespaced models in subdirectories as well as models
# in subdirectories without namespacing.
def get_model_class(file)
# this is for non-rails projects, which don't get Rails auto-require magic
require File.expand_path("#{model_dir}/#{file}") unless Module.const_defined?(:Rails)
require File.expand_path("#{model_dir}/#{file}")
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
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
......@@ -283,7 +334,7 @@ module AnnotateModels
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
......@@ -297,19 +348,17 @@ module AnnotateModels
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?
......@@ -325,22 +374,30 @@ module AnnotateModels
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
......@@ -351,18 +408,9 @@ module AnnotateModels
end
puts "Removed annotation from: #{deannotated.join(', ')}"
end
end
end
# monkey patches
module ::ActiveRecord
class Base
def self.method_missing(name, *args)
super
rescue NoMethodError => e
# ignore this, so unknown/unloaded macros won't cause parsing to fail
warn "Annotate Models ignoring #{e.class}: #{e.message}"
def find_test_file(dir, file_name)
Dir.glob(File.join(dir, "**", file_name)).first || File.join(dir, file_name)
end
end
end
Add a .rake file that automatically annotates models when you do a db:migrate
in development mode:
rails generate annotate_models:install
module AnnotateModels
module Generators
class InstallGenerator < Rails::Generators::Base
desc "Copy annotate_models rakefiles for automatic annotation"
source_root File.expand_path('../templates', __FILE__)
# copy rake tasks
def copy_tasks
template "auto_annotate_models.rake", "lib/tasks/auto_annotate_models.rake"
end
end
end
end
\ No newline at end of file
# NOTE: only doing this in development as some production environments (Heroku)
# NOTE: are sensitive to local FS writes, and besides -- it's just not proper
# NOTE: to have a dev-mode tool do its thing in production.
if(Rails.env.development?)
task :set_annotation_options do
ENV['position_in_class'] = "before"
ENV['position_in_fixture'] = "before"
ENV['position_in_factory'] = "before"
ENV['show_indexes'] = "true"
ENV['include_version'] = "false"
ENV['exclude_tests'] = "false"
ENV['exclude_fixtures'] = "false"
ENV['ignore_model_sub_dir'] = "false"
ENV['skip_on_db_migrate'] = "false"
ENV['format_rdoc'] = "false"
ENV['format_markdown'] = "false"
ENV['no_sort'] = "false"
ENV['force'] = "false"
end
end
annotate_lib = File.expand_path(File.dirname(File.dirname(__FILE__)))
if(!ENV['is_cli'])
task :set_annotation_options
task :annotate_models => :set_annotation_options
end
desc "Add schema information (as comments) to model and fixture files"
task :annotate_models => :environment do
require "#{annotate_lib}/annotate/annotate_models"
require "#{annotate_lib}/annotate/active_record_patch"
true_re = /(true|t|yes|y|1)$/i
options={}
options={ :is_rake => true }
options[:position_in_class] = ENV['position_in_class'] || ENV['position'] || 'before'
options[:position_in_fixture] = ENV['position_in_fixture'] || ENV['position'] || 'before'
options[:position_in_factory] = ENV['position_in_factory'] || ENV['position'] || 'before'
options[:show_indexes] = ENV['show_indexes'] =~ true_re
options[:simple_indexes] = ENV['simple_indexes'] =~ true_re
options[:model_dir] = ENV['model_dir']
......@@ -14,13 +23,19 @@ task :annotate_models => :environment do
options[:require] = ENV['require'] ? ENV['require'].split(',') : []
options[:exclude_tests] = ENV['exclude_tests'] =~ true_re
options[:exclude_fixtures] = ENV['exclude_fixtures'] =~ true_re
options[:ignore_model_sub_dir] = ENV['ignore_model_sub_dir'] =~ true_re
options[:format_rdoc] = ENV['format_rdoc'] =~ true_re
options[:format_markdown] = ENV['format_markdown'] =~ true_re
options[:no_sort] = ENV['no_sort'] =~ true_re
options[:force] = ENV['force'] =~ true_re
AnnotateModels.do_annotations(options)
end
desc "Remove schema information from model and fixture files"
task :remove_annotation => :environment do
require "#{annotate_lib}/annotate/annotate_models"
options={}
require "#{annotate_lib}/annotate/active_record_patch"
options={ :is_rake => true }
options[:model_dir] = ENV['model_dir']
AnnotateModels.remove_annotations(options)
end
desc "Prepends the route map to the top of routes.rb"
task :annotate_routes do
task :annotate_routes => :environment do
annotate_lib = File.expand_path(File.dirname(File.dirname(__FILE__)))
require "#{annotate_lib}/annotate/annotate_routes"
AnnotateRoutes.do_annotate
......
#encoding: utf-8
require File.dirname(__FILE__) + '/../spec_helper.rb'
require 'annotate/annotate_models'
require 'rubygems'
require 'active_support'
require 'annotate/active_record_patch'
describe AnnotateModels do
def mock_class(table_name, primary_key, columns)
options = {
:connection => mock("Conn", :indexes => []),
:table_name => table_name,
:primary_key => primary_key.to_s,
:column_names => columns.map { |col| col.name.to_s },
:columns => columns
}
def mock_klass(stubs={})
@mock_file ||= mock("Klass", stubs)
mock("An ActiveRecord class", options)
end
def mock_column(stubs={})
@mock_column ||= mock("Column", stubs)
def mock_column(name, type, options={})
default_options = {
:limit => nil,
:null => false,
:default => nil
}
stubs = default_options.dup
stubs.merge!(options)
stubs.merge!(:name => name, :type => type)
mock("Column", stubs)
end
it { AnnotateModels.quote(nil).should eql("NULL") }
......@@ -21,29 +38,45 @@ describe AnnotateModels do
it { AnnotateModels.quote(1e-20).should eql("1.0e-20") }
it "should get schema info" do
klass = mock_class(:users, :id, [
mock_column(:id, :integer),
mock_column(:name, :string, :limit => 50)
])
AnnotateModels.get_schema_info(mock_klass(
:connection => mock("Conn", :indexes => []),
:table_name => "users",
:primary_key => "id",
:column_names => ["id","login"],
:columns => [
mock_column(:type => "integer", :default => nil, :null => false, :name => "id", :limit => nil),
mock_column(:type => "string", :default => nil, :null => false, :name => "name", :limit => 50)
]), "Schema Info").should eql(<<-EOS)
AnnotateModels.get_schema_info(klass, "Schema Info").should eql(<<-EOS)
# Schema Info
#
# Table name: users
#
# id :integer not null, primary key
# id :integer not null, primary key
# name :string(50) not null
#
EOS
end
it "should get schema info as RDoc" do
klass = mock_class(:users, :id, [
mock_column(:id, :integer),
mock_column(:name, :string, :limit => 50)
])
AnnotateModels.get_schema_info(klass, AnnotateModels::PREFIX, :format_rdoc => true).should eql(<<-EOS)
# #{AnnotateModels::PREFIX}
#
# Table name: users
#
# *id*:: <tt>integer, not null, primary key</tt>
# *name*:: <tt>string(50), not null</tt>
#--
# #{AnnotateModels::END_MARK}
#++
EOS
end
describe "#get_model_class" do
require "tmpdir"
module ::ActiveRecord
class Base
def self.has_many name
......@@ -53,60 +86,234 @@ EOS
# todo: use 'files' gem instead
def create(file, body="hi")
File.open(@dir + '/' + file, "w") do |f|
file_path = File.join(AnnotateModels.model_dir, file)
FileUtils.mkdir_p(File.dirname(file_path))
File.open(file_path, "wb") do |f|
f.puts(body)
end
file_path
end
before :all do
require "tmpdir"
@dir = Dir.tmpdir + "/#{Time.now.to_i}" + "/annotate_models"
FileUtils.mkdir_p(@dir)
AnnotateModels.model_dir = @dir
create('foo.rb', <<-EOS)
def check_class_name(file, class_name)
klass = AnnotateModels.get_model_class(file)
klass.should_not == nil
klass.name.should == class_name
end
before :each do
AnnotateModels.model_dir = Dir.mktmpdir 'annotate_models'
end
it "should work" do
create 'foo.rb', <<-EOS
class Foo < ActiveRecord::Base
end
EOS
create('foo_with_macro.rb', <<-EOS)
check_class_name 'foo.rb', 'Foo'
end
it "should find models with non standard capitalization" do
create 'foo_with_capitals.rb', <<-EOS
class FooWithCAPITALS < ActiveRecord::Base
end
EOS
check_class_name 'foo_with_capitals.rb', 'FooWithCAPITALS'
end
it "should find models inside modules" do
create 'bar/foo_inside_bar.rb', <<-EOS
module Bar
class FooInsideBar < ActiveRecord::Base
end
end
EOS
check_class_name 'bar/foo_inside_bar.rb', 'Bar::FooInsideBar'
end
it "should not care about unknown macros" do
create 'foo_with_macro.rb', <<-EOS
class FooWithMacro < ActiveRecord::Base
acts_as_awesome :yah
end
EOS
check_class_name 'foo_with_macro.rb', 'FooWithMacro'
end
it "should not care about known macros" do
create('foo_with_known_macro.rb', <<-EOS)
class FooWithKnownMacro < ActiveRecord::Base
has_many :yah
end
EOS
check_class_name 'foo_with_known_macro.rb', 'FooWithKnownMacro'
end
it "should work with class names with ALL CAPS segments" do
create('foo_with_capitals.rb', <<-EOS)
class FooWithCAPITALS < ActiveRecord::Base
acts_as_awesome :yah
end
EOS
check_class_name 'foo_with_capitals.rb', 'FooWithCAPITALS'
end
it "should work" do
klass = AnnotateModels.get_model_class("foo.rb")
klass.name.should == "Foo"
it "should not complain of invalid multibyte char (USASCII)" do
create 'foo_with_utf8.rb', <<-EOS
#encoding: utf-8
class FooWithUtf8 < ActiveRecord::Base
UTF8STRINGS = %w[résumé façon âge]
end
EOS
check_class_name 'foo_with_utf8.rb', 'FooWithUtf8'
end
it "should not care about unknown macros" do
capturing(:stderr) do
klass = AnnotateModels.get_model_class("foo_with_macro.rb")
klass.name.should == "FooWithMacro"
end.should include("undefined method `acts_as_awesome'")
it "should find models inside modules with non standard capitalization" do
create 'bar/foo_inside_capitals_bar.rb', <<-EOS
module BAR
class FooInsideCapitalsBAR < ActiveRecord::Base
end
end
EOS
check_class_name 'bar/foo_inside_capitals_bar.rb', 'BAR::FooInsideCapitalsBAR'
end
it "should find non-namespaced models inside subdirectories" do
create 'bar/non_namespaced_foo_inside_bar.rb', <<-EOS
class NonNamespacedFooInsideBar < ActiveRecord::Base
end
EOS
check_class_name 'bar/non_namespaced_foo_inside_bar.rb', 'NonNamespacedFooInsideBar'
end
it "should find non-namespaced models with non standard capitalization inside subdirectories" do
create 'bar/non_namespaced_foo_with_capitals_inside_bar.rb', <<-EOS
class NonNamespacedFooWithCapitalsInsideBar < ActiveRecord::Base
end
EOS
check_class_name 'bar/non_namespaced_foo_with_capitals_inside_bar.rb', 'NonNamespacedFooWithCapitalsInsideBar'
end
it "should allow known macros" do
create('foo_with_known_macro.rb', <<-EOS)
class FooWithKnownMacro < ActiveRecord::Base
has_many :yah
end
EOS
capturing(:stderr) do
klass = AnnotateModels.get_model_class("foo_with_known_macro.rb")
klass.name.should == "FooWithKnownMacro"
check_class_name 'foo_with_known_macro.rb', 'FooWithKnownMacro'
end.should == ""
end
pending it "should find models with non standard capitalization" do
klass = AnnotateModels.get_model_class("foo_with_capitals.rb")
klass.name.should == "FooWithCAPITALS"
end
describe "#remove_annotation_of_file" do
require "tmpdir"
def create(file, body="hi")
path = File.join(@dir, file)
File.open(path, "w") do |f|
f.puts(body)
end
return path
end
def content(path)
File.read(path)
end
before :each do
@dir = Dir.mktmpdir 'annotate_models'
end
it "should remove before annotate" do
path = create "before.rb", <<-EOS
# == Schema Information
#
# Table name: foo
#
# id :integer not null, primary key
# created_at :datetime
# updated_at :datetime
#
class Foo < ActiveRecord::Base
end
EOS
AnnotateModels.remove_annotation_of_file(path)
content(path).should == <<-EOS
class Foo < ActiveRecord::Base
end
EOS
end
it "should remove after annotate" do
path = create "after.rb", <<-EOS
class Foo < ActiveRecord::Base
end
# == Schema Information
#
# Table name: foo
#
# id :integer not null, primary key
# created_at :datetime
# updated_at :datetime
#
EOS
AnnotateModels.remove_annotation_of_file(path)
content(path).should == <<-EOS
class Foo < ActiveRecord::Base
end
EOS
end
end
describe "annotating a file" do
before do
@file_name = File.join(Dir.mktmpdir('annotate_models'), "user.rb")
@file_content = <<-EOS
class User < ActiveRecord::Base
end
EOS
File.open(@file_name, "wb") { |f| f.write @file_content }
@klass = mock_class(:users, :id, [
mock_column(:id, :integer),
mock_column(:name, :string, :limit => 50)
])
@schema_info = AnnotateModels.get_schema_info(@klass, "== Schema Info")
end
it "should annotate the file before the model if position == 'before'" do
AnnotateModels.annotate_one_file(@file_name, @schema_info, :position => "before")
File.read(@file_name).should == "#{@schema_info}#{@file_content}"
end
it "should annotate before if given :position => :before" do
AnnotateModels.annotate_one_file(@file_name, @schema_info, :position => :before)
File.read(@file_name).should == "#{@schema_info}#{@file_content}"
end
it "should annotate before if given :position => :after" do
AnnotateModels.annotate_one_file(@file_name, @schema_info, :position => :after)
File.read(@file_name).should == "#{@file_content}\n#{@schema_info}"
end
it "should update annotate position" do
AnnotateModels.annotate_one_file(@file_name, @schema_info, :position => :before)
another_schema_info = AnnotateModels.get_schema_info(mock_class(:users, :id, [mock_column(:id, :integer),]),
"== Schema Info")
AnnotateModels.annotate_one_file(@file_name, another_schema_info, :position => :after)
File.read(@file_name).should == "#{@file_content}\n#{another_schema_info}"
end
end
end
--format=specdoc
--colour
\ No newline at end of file
......@@ -9,4 +9,6 @@ end
require "wrong/adapters/rspec"
$:.unshift(File.dirname(__FILE__) + '/../lib')
require 'active_support'
require 'active_support/core_ext/string/inflections'
require 'annotate'
TODO
-----
* clean up history
* change default position back to "top" for all annotations
* add "top" and "bottom" as synonyms for "before" and "after"
......@@ -22,4 +23,3 @@ TODO (proposed)
* supply two binaries, named 'annotate' and 'annotate_models', since there's already a unix tool named 'annotate'
* test EVERYTHING
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment