Commit 38ca3e7c by Chen Yujia

init commit

parents
!/spec/integration/*/log/.gitkeep
!/spec/integration/*/tmp/.gitkeep
*.gem
.DS_Store
.bundle
.gems
.rbenv-version
.ruby-*
/.idea/
/.rbx
/.rvmrc
/.yardoc/*
/Gemfile.lock
/coverage/*
/dist
/doc/*
/pkg/*
/spec/debug.log
/spec/integration/*/bin/
/spec/integration/*/db/test.*
/spec/integration/*/log/*
/spec/integration/*/tmp/*
.byebug_history
== Authors
- Original code by: Dave Thomas -- Pragmatic Programmers, LLC <http://agilewebdevelopment.com/plugins/annotate_models>
- Overhauled by: Alex Chaffee <http://alexch.github.com> alex@stinky.com
- Gemmed by: Cuong Tran <http://github.com/ctran> ctran@pragmaquest.com
- Maintained by: Alex Chaffee and Cuong Tran
- Homepage: http://github.com/ctran/annotate_models
With help from:
- 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
- Tsutomu Kuroda
- Kevin Moore
- Philip Hallstrom
- Brent Greeff
- Paul Alexander
- Dmitry Lihachev
- qichunren
- Guillermo Guerrero - http://github.com/ryanfox1985
and many others that I may have forgotten to add.
== 2.7.1
See https://github.com/ctran/annotate_models/releases/tag/v2.7.1
== 2.7.0
See https://github.com/ctran/annotate_models/releases/tag/v2.7.0
== 2.6.9
* Support foreigh key (#241)
* Check if model has skip tag in annotate_model_file (#167)
* Fix issue where serializer-related flags weren't being honored (#246)
* Prefer SQL column type over normalized AR type (#231)
== 2.6.8
* Nothing annotated unless options[:model_dir] is specified, #234
== 2.6.7
* Nothing annotated unless options[:model_dir] is specified, #234
== 2.6.6
* Makes it possible to wrap annotations, #225
* Fix single model generation, #214
* Fix default value for Rails 4.2, #212
* Don't crash on inherited models in subdirectories, #232
* Process model_dir in rake task, #197
== 2.6.4
* Skip "models/concerns", #194
* Fix #173 where annotate says "Nothing to annotate" in rails 4.2
* Display an error message if not run from the root of the project, #186
* Support rails 4.0 new default test directory, #182
* Add an option to show timestamp in routes "-timestamp", #136
* Skip plain ruby objects if they have the same class name as an ActiveRecord object, #121
== 2.6.3
* Fix bug of annotate position in routes (#158)
== 2.6.2
* Retain the current annotate block unless --force is specified
* Always load models, since they may not be autoloaded by Rails
* The pg array type is now detected (see #158)
== 2.6.0.beta2
* support for composite_primary_keys (garysweaver)
* bug fix for annotate_one_file (vlado)
== 2.6.0.beta1
* It's now possible to use Annotate in standalone ActiveRecord (non-Rails)
projects again.
* Adding note that Markdown is actually MultiMarkdown, and recommending the use
of the `kramdown` engine for parsing it.
* Improved Markdown formatting considerably.
* Bugfix: Needed to use inline-code tag for column and table names, otherwise
underscores would cause havok with the formatting.
* Bugfix: Markdown syntax was incorrect (can't have trailing spaces before the
closing marker for an emphasis tag).
* Bugfix: Remove-annotations wasn't properly finding test/spec files, and
wasn't even looking for FactoryGirl factories under the new naming
convention.
* Bugfix: Load the Rakefile from the current directory, not the first Rakefile
in our load path.
* Added support for new FactoryGirl naming convention.
* Fix behavior of route annotations in newer versions of Rake that don't spit
out the CWD as their first line of output.
* Overhauled integration testing system to be much easier to work with, better
compartmentalized, and so forth -- at the cost that you must be using RVM to
utilize it. (It'll spit out appropriate pending messages if you don't.)
Also includes a mode for "tinkering" by hand with a scenario, and won't let
you run it through rspect if the repo is in a dirty state. Added appropriate
rake tasks to help with all of this.
* Routes can now be appended, pre-pended, or removed -- and do sane things in
all cases.
* Expose all `position_*` variables as CLI params.
* Make `ENV ['position']` work as a default for all the `ENV ['position_*']`
variables.
* Make rake tasks more resilient to unusual circumstances / code loading
behavior.
* Resolve annotate vs. annotate_models ambiguity once and for all by settling
on `annotate_models` _and_ `annotate_routes`. This avoids a name collision
with RMagick while not needlessly overloading the term.
* Fixed that schema kept prepending additional newlines
* Updates to make annotate smarter about when to touch a model
* Recognize column+type, and don't change a file unless the column+type
combination of the new schema are different than that of the old (i.e., don't
regenerate if columns happen to be in a different order. That's just how life
is sometimes)
* Change annotate to use options hash instead of ENV.
== 2.5.0
* Works better with Rails 3
* Bugfix: schema kept prepending additional newlines
* Updates to make annotate smarter about when to touch a model
* Recognize column+type, and don't change a file unless the column+type combination of the new schema are different than that of the old (i.e., don't regenerate if columns happen to be in a different order. That's just how life is sometimes.)
* Grab old specification even if it has \r\n as line endings rather than pure \ns
* Various warning and specification fixes
* Fix "no such file to load -- annotate/annotate_models (MissingSourceFile)"
error (require statements in tasks now use full path to lib files)
* warn about macros, to mitigate when we're included during a production run,
not just a rakefile run -- possibly at the expense of too much noise
* Adding rake as a runtime dependency
* If the schema is already in the model file, it will be replaced into the same
location. If it didn't previously exist, it'll be placed according to the
"position", as before.
* Allow task loading from Rakefile for gems (plugin installation already
auto-detects).
* Add skip_on_db_migrate option as well for people that don't want it
* Fix options parsing to convert strings to proper booleans
* Add support for Fabrication fabricators
* Leave magic encoding comment intact
* Fix issue #14 - RuntimeError: Already memoized
* Count a model as 'annotated' if any of its tests/fixtures are annotated
* 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. Use --sort if you 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.
* 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.
* Now works on tables with no primary key.
* --format=markdown option
* --trace option to help debug "Unable to annotate" errors
* "Table name" annotation (if table name is different from model name)
* "Human name" annotation (enabling translation to non-English locales)
* Fix JRuby ObjectSpace compatibility bug (https://github.com/ctran/annotate_models/pull/85)
* Fix FactoryGirl compatibility bug (https://github.com/ctran/annotate_models/pull/82)
== 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
== 2.4.0 2009-12-13
* Incorporated lots of patches from the Github community, including support for
Blueprints fixtures
* Several bug fixes
== 2.1 2009-10-18
* New options
* -R to require additional files before loading the models
* -i to show database indexes in annotations
* -e to exclude annotating tests or fixtures
* -m to include the migration version number in the annotation
* --model-dir to annotate model files stored a different place than app/models
* Ignore unknown macros ('acts_as_whatever')
== 2.0 2009-02-03
* Add annotate_models plugin fork additions
* Annotates Rspec and Test Unit models
* Annotates Object Daddy exemplars
* Annotates geometrical columns
* Add AnnotateRoutes rake task
* Up gem structure to newgem defaults
== 1.0.4 2008-09-04
* Only update modified models since last run, thanks to sant0sk1
== 1.0.3 2008-05-02
* Add misc changes from Dustin Sallings and Henrik N
* Remove trailing whitespace
* More intuitive info messages
* Update README file with update-to-date example
== 1.0.2 2008-03-22
* Add contributions from Michael Bumann (http://github.com/bumi)
* added an option "position" to choose to put the annotation,
* spec/fixtures now also get annotated
* added a task to remove the annotations
* these options can be specified from command line as -d and -p [before|after]
You can redistribute it and/or modify it under either the terms of the
2-clause BSDL (see the file BSDL), or the conditions below:
1. You may make and give away verbatim copies of the source form of the
software without restriction, provided that you duplicate all of the
original copyright notices and associated disclaimers.
2. You may modify your copy of the software in any way, provided that
you do at least ONE of the following:
a) place your modifications in the Public Domain or otherwise
make them Freely Available, such as by posting said
modifications to Usenet or an equivalent medium, or by allowing
the author to include your modifications in the software.
b) use the modified software only within your corporation or
organization.
c) give non-standard binaries non-standard names, with
instructions on where to get the original software distribution.
d) make other distribution arrangements with the author.
3. You may distribute the software in object code or binary form,
provided that you do at least ONE of the following:
a) distribute the binaries and library files of the software,
together with instructions (in the manual page or equivalent)
on where to get the original distribution.
b) accompany the distribution with the machine-readable source of
the software.
c) give non-standard binaries non-standard names, with
instructions on where to get the original software distribution.
d) make other distribution arrangements with the author.
4. You may modify and include the part of the software into any other
software (possibly commercial). But some files in the distribution
are not written by the author, so that they are not under these terms.
For the list of those files and their copying conditions, see the
file LEGAL.
5. The scripts and library files supplied as input to or produced as
output from the software do not automatically fall under the
copyright of the software, but belong to whomever generated them,
and may be sold commercially, and may be aggregated with this
software.
6. THIS SOFTWARE IS PROVIDED "AS IS" AND WITHOUT ANY EXPRESS OR
IMPLIED WARRANTIES, INCLUDING, WITHOUT LIMITATION, THE IMPLIED
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
PURPOSE.
\ No newline at end of file
== Annotate (aka AnnotateModels)
{<img src="https://badge.fury.io/rb/annotate.svg" alt="Gem Version" />}[http://badge.fury.io/rb/annotate]
{<img src="https://img.shields.io/gem/dt/annotate.svg?style=flat" />}[https://rubygems.org/gems/annotate]
{<img src="https://travis-ci.org/ctran/annotate_models.png" />}[https://travis-ci.org/ctran/annotate_models]
{<img src="https://coveralls.io/repos/ctran/annotate_models/badge.svg?branch=develop" />}[https://coveralls.io/r/ctran/annotate_models?branch=develop]
{<img src="https://codeclimate.com/github/ctran/annotate_models/badges/gpa.svg" />}[https://codeclimate.com/github/ctran/annotate_models]
{<img src="https://gemnasium.com/ctran/annotate_models.png" />}[https://gemnasium.com/ctran/annotate_models]
Add a comment summarizing the current schema to the top or bottom of each of
your...
- ActiveRecord models
- Fixture files
- Tests and Specs
- Object Daddy exemplars
- Machinist blueprints
- Fabrication fabricators
- Thoughtbot's factory_girl factories, i.e. the (spec|test)/factories/<model>_factory.rb files
- routes.rb file (for Rails projects)
The schema comment looks like this:
# == Schema Info
#
# Table name: line_items
#
# id :integer(11) not null, primary key
# quantity :integer(11) not null
# product_id :integer(11) not null
# unit_price :float
# order_id :integer(11)
#
class LineItem < ActiveRecord::Base
belongs_to :product
. . .
It also annotates geometrical columns, geom type and srid, when using
+SpatialAdapter+, +PostgisAdapter+ or +PostGISAdapter+:
# == Schema Info
#
# Table name: trips
#
# local :geometry point, 4326
# path :geometry line_string, 4326
Also, if you pass the -r option, it'll annotate routes.rb with the output of
<code>rake routes</code>.
== Install
Into Gemfile from rubygems.org:
gem 'annotate'
Into Gemfile from Github:
gem 'annotate', github: 'ctran/annotate_models'
Into environment gems from rubygems.org:
gem install annotate
Into environment gems from Github checkout:
git clone git://github.com/ctran/annotate_models.git annotate_models
cd annotate_models
rake build
gem install pkg/annotate-*.gem
== Usage
(If you used the Gemfile install, prefix the below commands with <code>bundle exec</code>.)
=== Usage in Rails
To annotate all your models, tests, fixtures, and factories:
cd /path/to/app
annotate
To annotate just your models, tests, and factories:
annotate --exclude fixtures
To annotate just your models:
annotate --exclude tests,fixtures,factories,serializers
To annotate routes.rb:
annotate --routes
To remove model/test/fixture/factory/serializer annotations:
annotate --delete
To remove routes.rb annotations:
annotate --routes --delete
To automatically annotate every time you run <code>db:migrate</code>, either run <code>rails g annotate:install</code> or add +Annotate.load_tasks+ to your `Rakefile`. See the {configuration in Rails}[link:README.rdoc#configuration-in-rails] section for more info.
=== Usage Outside of Rails
Everything above applies, except that +--routes+ is not meaningful, and you will
probably need to explicitly set one or more +--require+ option(s), and/or one
or more +--model-dir+ options to inform annotate about the structure of your
project and help it bootstrap and load the relevant code.
== Configuration
If you want to always skip annotations on a particular model, add this string
anywhere in the file:
# -*- SkipSchemaAnnotations
=== Configuration in Rails
To generate a configuration file (in the form of a +.rake+ file), to set
default options:
rails g annotate:install
Edit this file to control things like output format, where annotations are
added (top or bottom of file), and in which artifacts.
The generated rakefile +lib/tasks/auto_annotate_models.rake+ also contains
`Annotate.load_tasks`. This adds a few rake tasks which duplicate command-line
functionality:
rake annotate_models # Add schema information (as comments) to model and fixture files
rake annotate_routes # Adds the route map to routes.rb
rake remove_annotation # Remove schema information from model and fixture files
By default, once you've generated a configuration file, annotate will be
executed whenever you run <code>rake db:migrate</code> (but only in development mode).
If you want to disable this behavior permanently, edit the +.rake+ file and
change:
'skip_on_db_migrate' => 'false',
To:
'skip_on_db_migrate' => 'true',
If you want to run <code>rake db:migrate</code> as a one-off without running annotate,
you can do so with a simple environment variable, instead of editing the
+.rake+ file:
skip_on_db_migrate=1 rake db:migrate
== Options
Usage: annotate [options] [model_file]*
-d, --delete Remove annotations from all model files or the routes.rb file
-p, --position [before|top|after|bottom] Place the annotations at the top (before) or the bottom (after) of the model/test/fixture/factory/routes file(s)
--pc, --position-in-class [before|top|after|bottom]
Place the annotations at the top (before) or the bottom (after) of the model file
--pf, --position-in-factory [before|top|after|bottom]
Place the annotations at the top (before) or the bottom (after) of any factory files
--px, --position-in-fixture [before|top|after|bottom]
Place the annotations at the top (before) or the bottom (after) of any fixture files
--pt, --position-in-test [before|top|after|bottom]
Place the annotations at the top (before) or the bottom (after) of any test files
--pr, --position-in-routes [before|top|after|bottom]
Place the annotations at the top (before) or the bottom (after) of the routes.rb file
--ps, --position-in-serializer [before|top|after|bottom]
Place the annotations at the top (before) or the bottom (after) of the serializer files
--w, --wrapper STR Wrap annotation with the text passed as parameter.
If --w option is used, the same text will be used as opening and closing
--wo, --wrapper-open STR Annotation wrapper opening.
--wc, --wrapper-close STR Annotation wrapper closing
-r, --routes Annotate routes.rb with the output of 'rake routes'
-aa, --active-admin Annotate all activeadmin models
-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
-k, --show-foreign-keys List the table's foreign key constraints in the annotation
-s, --simple-indexes Concat the column's related indexes in the annotation
--model-dir dir Annotate model files stored in dir rather than app/models, separate multiple dirs with comas
--ignore-model-subdirects Ignore subdirectories of the models directory
--sort Sort columns alphabetically, rather than in creation order
-R, --require path Additional file to require before loading models, may be used multiple times
-e [tests,fixtures,factories,serializers],
--exclude Do not annotate fixtures, test files, factories, and/or serializers
-f [bare|rdoc|markdown], Render Schema Infomation as plain/RDoc/Markdown
--format
--force Force new annotations even if there are no changes.
--timestamp Include timestamp in (routes) annotation
--trace If unable to annotate a file, print the full stack trace, not just the exception message.
-I, --ignore-columns REGEX don't annotate columns that match a given REGEX (i.e., `annotate -I '^(id|updated_at|created_at)'`
== Sorting
By default, columns will be sorted in database order (i.e. the order in which
migrations were run).
If you prefer to sort alphabetically so that the results of
annotation are consistent regardless of what order migrations are executed in,
use +--sort+.
== Markdown
The format produced is actually MultiMarkdown, making use of the syntax
extension for tables. It's recommended you use +kramdown+ as your parser if
you want to use this format. If you're using +yard+ to generate documentation,
specify a format of markdown with +kramdown+ as the provider by adding this to
your +.yardopts+ file:
--markup markdown
--markup-provider kramdown
Be sure to add this to your +Gemfile+ as well:
gem 'kramdown', :groups => [:development], :require => false
== WARNING
<b>Don't add text after an automatically-created comment block.</b> This tool
will blow away the initial/final comment block in your models if it looks like
it was previously added by this gem.
Be sure to check the changes that this tool makes! If you are using Git,
you may simply check your project's status after running +annotate+:
$ git status
If you are not using a VCS (like Git, Subversion or similar), please tread
extra carefully, and consider using one.
== Links
- 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
- PostGISAdapter: https://github.com/dazuma/activerecord-postgis-adapter
== License
Released under the same license as Ruby. No Support. No Warranty.
== Authors
{See AUTHORS.rdoc}[link:AUTHORS.rdoc].
== TODO
- clean up history
- change default position back to "top" for all annotations
- change 'exclude' to 'only' (double negatives are not unconfusing)
== TODO (proposed)
- push two identical gems, named 'annotate' and 'annotate_models'
- supply two binaries, named 'annotate' and 'annotate_models', since there's already a unix tool named 'annotate'
- test EVERYTHING
# -*- encoding: utf-8 -*-
lib = File.expand_path('../lib', __FILE__)
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
require 'annotate/version'
Gem::Specification.new do |s|
s.name = "annotate"
s.version = Annotate.version
s.required_ruby_version = '>= 1.9.3'
s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
s.authors = ["Alex Chaffee", "Cuong Tran", "Marcos Piccinini", "Turadg Aleahmad", "Jon Frisby"]
s.description = "Annotates Rails/ActiveRecord Models, routes, fixtures, and others based on the database schema."
s.email = ["alex@stinky.com", "cuong.tran@gmail.com", "x@nofxx.com", "turadg@aleahmad.net", "jon@cloudability.com"]
s.executables = ["annotate"]
s.extra_rdoc_files = ["README.rdoc", "CHANGELOG.rdoc", "TODO.rdoc"]
s.files = ["AUTHORS.rdoc", "CHANGELOG.rdoc", "LICENSE.txt", "README.rdoc", "TODO.rdoc", "annotate.gemspec", "bin/annotate", "lib/annotate.rb", "lib/annotate/active_record_patch.rb", "lib/annotate/annotate_models.rb", "lib/annotate/annotate_routes.rb", "lib/annotate/tasks.rb", "lib/annotate/version.rb", "lib/generators/annotate/USAGE", "lib/generators/annotate/install_generator.rb", "lib/generators/annotate/templates/auto_annotate_models.rake", "lib/tasks/annotate_models.rake", "lib/tasks/annotate_routes.rake", "lib/tasks/migrate.rake"]
s.homepage = "http://github.com/ctran/annotate_models"
s.licenses = ["Ruby"]
s.require_paths = ["lib"]
s.rubyforge_project = "annotate"
s.rubygems_version = "2.1.11"
s.summary = "Annotates Rails Models, routes, fixtures, and others based on the database schema."
if s.respond_to? :specification_version then
s.specification_version = 4
if Gem::Version.new(Gem::VERSION) >= Gem::Version.new('1.2.0') then
s.add_runtime_dependency(%q<rake>, [">= 10.4", "< 12.0"])
s.add_runtime_dependency(%q<activerecord>, [">= 3.2", "< 6.0"])
else
s.add_dependency(%q<rake>, [">= 10.4", "< 12.0"])
s.add_dependency(%q<activerecord>, [">= 3.2", "< 6.0"])
end
else
s.add_dependency(%q<rake>, [">= 0.8.7"])
s.add_dependency(%q<activerecord>, [">= 3.2", "< 6.0"])
end
end
#!/usr/bin/env ruby
unless File.exist?('./Rakefile') || File.exist?('./Gemfile')
abort 'Please run annotate from the root of the project.'
end
require 'rubygems'
begin
require 'bundler'
Bundler.setup
rescue Exception
end
here = File.expand_path(File.dirname __FILE__)
$:<< "#{here}/../lib"
require 'optparse'
require 'annotate'
Annotate.bootstrap_rake
has_set_position = {}
target_action = :do_annotations
positions = %w(before top after bottom)
OptionParser.new do |opts|
opts.banner = 'Usage: annotate [options] [model_file]*'
opts.on('-d', '--delete', 'Remove annotations from all model files or the routes.rb file') do
target_action = :remove_annotations
end
opts.on('-p', '--position [before|top|after|bottom]', positions,
'Place the annotations at the top (before) or the bottom (after) of the model/test/fixture/factory/route/serializer file(s)') do |p|
ENV['position'] = p
%w(position_in_class position_in_factory position_in_fixture position_in_test position_in_routes position_in_serializer).each do |key|
ENV[key] = p unless (has_set_position[key])
end
end
opts.on('--pc', '--position-in-class [before|top|after|bottom]', positions,
'Place the annotations at the top (before) or the bottom (after) of the model file') do |p|
ENV['position_in_class'] = p
has_set_position['position_in_class'] = true
end
opts.on('--pf', '--position-in-factory [before|top|after|bottom]', positions,
'Place the annotations at the top (before) or the bottom (after) of any factory files') do |p|
ENV['position_in_factory'] = p
has_set_position['position_in_factory'] = true
end
opts.on('--px', '--position-in-fixture [before|top|after|bottom]', positions,
'Place the annotations at the top (before) or the bottom (after) of any fixture files') do |p|
ENV['position_in_fixture'] = p
has_set_position['position_in_fixture'] = true
end
opts.on('--pt', '--position-in-test [before|top|after|bottom]', positions,
'Place the annotations at the top (before) or the bottom (after) of any test files') do |p|
ENV['position_in_test'] = p
has_set_position['position_in_test'] = true
end
opts.on('--pr', '--position-in-routes [before|top|after|bottom]', positions,
'Place the annotations at the top (before) or the bottom (after) of the routes.rb file') do |p|
ENV['position_in_routes'] = p
has_set_position['position_in_routes'] = true
end
opts.on('--ps', '--position-in-serializer [before|top|after|bottom]', positions,
'Place the annotations at the top (before) or the bottom (after) of the serializer files') do |p|
ENV['position_in_serializer'] = p
has_set_position['position_in_serializer'] = true
end
opts.on('--w', '--wrapper STR', 'Wrap annotation with the text passed as parameter.',
'If --w option is used, the same text will be used as opening and closing') do |p|
ENV['wrapper'] = p
end
opts.on('--wo', '--wrapper-open STR', 'Annotation wrapper opening.') do |p|
ENV['wrapper_open'] = p
end
opts.on('--wc', '--wrapper-close STR', 'Annotation wrapper closing') do |p|
ENV['wrapper_close'] = p
end
opts.on('-r', '--routes', "Annotate routes.rb with the output of 'rake routes'") do
ENV['routes'] = 'true'
end
opts.on('-aa', '--active-admin', 'Annotate active_admin models') do |p|
ENV['active_admin'] = p
end
opts.on('-v', '--version',
'Show the current version of this gem') do
puts "annotate v#{Annotate.version}"; exit
end
opts.on('-m', '--show-migration', 'Include the migration version number in the annotation') do
ENV['include_version'] = 'yes'
end
opts.on('-k', '--show-foreign-keys',
"List the table's foreign key constraints in the annotation") do
ENV['show_foreign_keys'] = 'yes'
end
opts.on('-i', '--show-indexes',
"List the table's database indexes in the annotation") do
ENV['show_indexes'] = 'yes'
end
opts.on('-s', '--simple-indexes',
"Concat the column's related indexes in the annotation") do
ENV['simple_indexes'] = 'yes'
end
opts.on('--model-dir dir',
"Annotate model files stored in dir rather than app/models, separate multiple dirs with comas") do |dir|
ENV['model_dir'] = dir
end
opts.on('--root-dir dir',
"Annotate files stored within root dir projects, separate multiple dirs with comas") do |dir|
ENV['root_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('--sort',
"Sort columns alphabetically, rather than in creation order") do |dir|
ENV['sort'] = 'yes'
end
opts.on('--classified-sort',
"Sort columns alphabetically, but first goes id, then the rest columns, then the timestamp columns and then the association columns") do |dir|
ENV['classified_sort'] = 'yes'
end
opts.on('-R', '--require path',
"Additional file to require before loading models, may be used multiple times") do |path|
if !ENV['require'].blank?
ENV['require'] = ENV['require'] + ",#{path}"
else
ENV['require'] = path
end
end
opts.on('-e', '--exclude [tests,fixtures,factories,serializers]', Array, "Do not annotate fixtures, test files, factories, and/or serializers") do |exclusions|
exclusions ||= %w(tests fixtures factories)
exclusions.each { |exclusion| ENV["exclude_#{exclusion}"] = 'yes' }
end
opts.on('-f', '--format [bare|rdoc|markdown]', %w(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
opts.on('--timestamp', 'Include timestamp in (routes) annotation') do
ENV['timestamp'] = 'true'
end
opts.on('--trace', 'If unable to annotate a file, print the full stack trace, not just the exception message.') do |value|
ENV['trace'] = 'yes'
end
opts.on('-I', '--ignore-columns REGEX', "don't annotate columns that match a given REGEX (i.e., `annotate -I '^(id|updated_at|created_at)'`") do |regex|
ENV['ignore_columns'] = regex
end
opts.on('--ignore-routes REGEX', "don't annotate routes that match a given REGEX (i.e., `annotate -I '(mobile|resque|pghero)'`") do |regex|
ENV['ignore_routes'] = regex
end
opts.on('--hide-limit-column-types VALUES', "don't show limit for given column types, separated by comas (i.e., `integer,boolean,text`)") do |values|
ENV['hide_limit_column_types'] = "#{values}"
end
opts.on('--ignore-unknown-models', "don't display warnings for bad model files") do |values|
ENV['ignore_unknown_models'] = 'true'
end
end.parse!
options = Annotate.setup_options({is_rake: ENV['is_rake'] && !ENV['is_rake'].empty?})
Annotate.eager_load(options)
AnnotateModels.send(target_action, options) if Annotate.include_models?
AnnotateRoutes.send(target_action, options) if Annotate.include_routes?
$LOAD_PATH.unshift(File.dirname(__FILE__))
require 'annotate/version'
require 'annotate/annotate_models'
require 'annotate/annotate_routes'
begin
# ActiveSupport 3.x...
require 'active_support/hash_with_indifferent_access'
require 'active_support/core_ext/object/blank'
rescue Exception
# ActiveSupport 2.x...
require 'active_support/core_ext/hash/indifferent_access'
require 'active_support/core_ext/blank'
end
module Annotate
TRUE_RE = /^(true|t|yes|y|1)$/i
##
# The set of available options to customize the behavior of Annotate.
#
POSITION_OPTIONS = [
:position_in_routes, :position_in_class, :position_in_test,
:position_in_fixture, :position_in_factory, :position,
:position_in_serializer
].freeze
FLAG_OPTIONS = [
:show_indexes, :simple_indexes, :include_version, :exclude_tests,
:exclude_fixtures, :exclude_factories, :ignore_model_sub_dir,
:format_bare, :format_rdoc, :format_markdown, :sort, :force, :trace,
:timestamp, :exclude_serializers, :classified_sort, :show_foreign_keys,
:exclude_scaffolds, :exclude_controllers, :exclude_helpers, :ignore_unknown_models
].freeze
OTHER_OPTIONS = [
:ignore_columns, :skip_on_db_migrate, :wrapper_open, :wrapper_close, :wrapper, :routes,
:hide_limit_column_types, :ignore_routes, :active_admin
].freeze
PATH_OPTIONS = [
:require, :model_dir, :root_dir
].freeze
##
# Set default values that can be overridden via environment variables.
#
def self.set_defaults(options = {})
return if @has_set_defaults
@has_set_defaults = true
options = HashWithIndifferentAccess.new(options)
[POSITION_OPTIONS, FLAG_OPTIONS, PATH_OPTIONS, OTHER_OPTIONS].flatten.each do |key|
if options.key?(key)
default_value = if options[key].is_a?(Array)
options[key].join(',')
else
options[key]
end
end
default_value = ENV[key.to_s] unless ENV[key.to_s].blank?
ENV[key.to_s] = default_value.nil? ? nil : default_value.to_s
end
end
##
# TODO: what is the difference between this and set_defaults?
#
def self.setup_options(options = {})
POSITION_OPTIONS.each do |key|
options[key] = fallback(ENV[key.to_s], ENV['position'], 'before')
end
FLAG_OPTIONS.each do |key|
options[key] = true?(ENV[key.to_s])
end
OTHER_OPTIONS.each do |key|
options[key] = !ENV[key.to_s].blank? ? ENV[key.to_s] : nil
end
PATH_OPTIONS.each do |key|
options[key] = !ENV[key.to_s].blank? ? ENV[key.to_s].split(',') : []
end
options[:model_dir] = ['app/models'] if options[:model_dir].empty?
options[:root_dir] = [''] if options[:root_dir].empty?
options[:wrapper_open] ||= options[:wrapper]
options[:wrapper_close] ||= options[:wrapper]
# These were added in 2.7.0 but so this is to revert to old behavior by default
options[:exclude_scaffolds] = Annotate.true?(ENV.fetch('exclude_scaffolds', 'true'))
options[:exclude_controllers] = Annotate.true?(ENV.fetch('exclude_controllers', 'true'))
options[:exclude_helpers] = Annotate.true?(ENV.fetch('exclude_helpers', 'true'))
options
end
def self.reset_options
[POSITION_OPTIONS, FLAG_OPTIONS, PATH_OPTIONS, OTHER_OPTIONS].flatten.each do |key|
ENV[key.to_s] = nil
end
end
def self.skip_on_migration?
ENV['skip_on_db_migrate'] =~ TRUE_RE
end
def self.include_routes?
ENV['routes'] =~ TRUE_RE
end
def self.include_models?
true
end
def self.loaded_tasks=(val)
@loaded_tasks = val
end
def self.loaded_tasks
@loaded_tasks
end
def self.load_tasks
return if loaded_tasks
self.loaded_tasks = true
Dir[File.join(File.dirname(__FILE__), 'tasks', '**/*.rake')].each { |rake| load rake }
end
def self.load_requires(options)
options[:require].each { |path| require path } if options[:require].count > 0
end
def self.eager_load(options)
load_requires(options)
require 'annotate/active_record_patch'
if defined?(Rails)
if Rails.version.split('.').first.to_i < 3
Rails.configuration.eager_load_paths.each do |load_path|
matcher = /\A#{Regexp.escape(load_path)}(.*)\.rb\Z/
Dir.glob("#{load_path}/**/*.rb").sort.each do |file|
require_dependency file.sub(matcher, '\1')
end
end
else
klass = Rails::Application.send(:subclasses).first
klass.eager_load!
end
else
options[:model_dir].each do |dir|
FileList["#{dir}/**/*.rb"].each do |fname|
require File.expand_path(fname)
end
end
end
end
def self.bootstrap_rake
begin
require 'rake/dsl_definition'
rescue Exception => e
# We might just be on an old version of Rake...
puts e.message
exit e.status_code
end
require 'rake'
load './Rakefile' if File.exist?('./Rakefile')
begin
Rake::Task[:environment].invoke
rescue
nil
end
unless defined?(Rails)
# Not in a Rails project, so time to load up the parts of
# ActiveSupport we need.
require 'active_support'
require 'active_support/core_ext/class/subclasses'
require 'active_support/core_ext/string/inflections'
end
load_tasks
Rake::Task[:set_annotation_options].invoke
end
def self.fallback(*args)
args.detect { |arg| !arg.blank? }
end
def self.true?(val)
return false if val.blank?
return false unless val =~ TRUE_RE
true
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
end
end
end
require 'bigdecimal'
module AnnotateModels
TRUE_RE = /^(true|t|yes|y|1)$/i
# 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"
MATCHED_TYPES = %w(test fixture factory serializer scaffold controller helper)
# File.join for windows reverse bar compat?
# I dont use windows, can`t test
UNIT_TEST_DIR = File.join("test", "unit")
MODEL_TEST_DIR = File.join("test", "models") # since rails 4.0
SPEC_MODEL_DIR = File.join("spec", "models")
FIXTURE_TEST_DIR = File.join("test", "fixtures")
FIXTURE_SPEC_DIR = File.join("spec", "fixtures")
# Other test files
CONTROLLER_TEST_DIR = File.join("test", "controllers")
CONTROLLER_SPEC_DIR = File.join("spec", "controllers")
REQUEST_SPEC_DIR = File.join("spec", "requests")
ROUTING_SPEC_DIR = File.join("spec", "routing")
# 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")
# Serializers https://github.com/rails-api/active_model_serializers
SERIALIZERS_DIR = File.join("app", "serializers")
SERIALIZERS_TEST_DIR = File.join("test", "serializers")
SERIALIZERS_SPEC_DIR = File.join("spec", "serializers")
# Controller files
CONTROLLER_DIR = File.join("app", "controllers")
# Active admin registry files
ACTIVEADMIN_DIR = File.join("app", "admin")
# Helper files
HELPER_DIR = File.join("app", "helpers")
# Don't show limit (#) on these column types
# Example: show "integer" instead of "integer(4)"
NO_LIMIT_COL_TYPES = %w(integer boolean)
# Don't show default value for these column types
NO_DEFAULT_COL_TYPES = %w(json jsonb)
class << self
def annotate_pattern(options = {})
if options[:wrapper_open]
return /(?:^\n?# (?:#{options[:wrapper_open]}).*\n?# (?:#{COMPAT_PREFIX}|#{COMPAT_PREFIX_MD}).*?\n(#.*\n)*\n*)|^\n?# (?:#{COMPAT_PREFIX}|#{COMPAT_PREFIX_MD}).*?\n(#.*\n)*\n*/
end
/^\n?# (?:#{COMPAT_PREFIX}|#{COMPAT_PREFIX_MD}).*?\n(#.*\n)*\n*/
end
def model_dir
@model_dir.is_a?(Array) ? @model_dir : [@model_dir || 'app/models']
end
attr_writer :model_dir
def root_dir
@root_dir.is_a?(Array) ? @root_dir : [@root_dir || '']
end
attr_writer :root_dir
def test_files(root_directory)
[
File.join(root_directory, UNIT_TEST_DIR, "%MODEL_NAME%_test.rb"),
File.join(root_directory, MODEL_TEST_DIR, "%MODEL_NAME%_test.rb"),
File.join(root_directory, SPEC_MODEL_DIR, "%MODEL_NAME%_spec.rb")
]
end
def fixture_files(root_directory)
[
File.join(root_directory, FIXTURE_TEST_DIR, "%TABLE_NAME%.yml"),
File.join(root_directory, FIXTURE_SPEC_DIR, "%TABLE_NAME%.yml"),
File.join(root_directory, FIXTURE_TEST_DIR, "%PLURALIZED_MODEL_NAME%.yml"),
File.join(root_directory, FIXTURE_SPEC_DIR, "%PLURALIZED_MODEL_NAME%.yml")
]
end
def scaffold_files(root_directory)
[
File.join(root_directory, CONTROLLER_TEST_DIR, "%PLURALIZED_MODEL_NAME%_controller_test.rb"),
File.join(root_directory, CONTROLLER_SPEC_DIR, "%PLURALIZED_MODEL_NAME%_controller_spec.rb"),
File.join(root_directory, REQUEST_SPEC_DIR, "%PLURALIZED_MODEL_NAME%_spec.rb"),
File.join(root_directory, ROUTING_SPEC_DIR, "%PLURALIZED_MODEL_NAME%_routing_spec.rb")
]
end
def factory_files(root_directory)
[
File.join(root_directory, EXEMPLARS_TEST_DIR, "%MODEL_NAME%_exemplar.rb"),
File.join(root_directory, EXEMPLARS_SPEC_DIR, "%MODEL_NAME%_exemplar.rb"),
File.join(root_directory, BLUEPRINTS_TEST_DIR, "%MODEL_NAME%_blueprint.rb"),
File.join(root_directory, BLUEPRINTS_SPEC_DIR, "%MODEL_NAME%_blueprint.rb"),
File.join(root_directory, FACTORY_GIRL_TEST_DIR, "%MODEL_NAME%_factory.rb"), # (old style)
File.join(root_directory, FACTORY_GIRL_SPEC_DIR, "%MODEL_NAME%_factory.rb"), # (old style)
File.join(root_directory, FACTORY_GIRL_TEST_DIR, "%TABLE_NAME%.rb"), # (new style)
File.join(root_directory, FACTORY_GIRL_SPEC_DIR, "%TABLE_NAME%.rb"), # (new style)
File.join(root_directory, FABRICATORS_TEST_DIR, "%MODEL_NAME%_fabricator.rb"),
File.join(root_directory, FABRICATORS_SPEC_DIR, "%MODEL_NAME%_fabricator.rb")
]
end
def serialize_files(root_directory)
[
File.join(root_directory, SERIALIZERS_DIR, "%MODEL_NAME%_serializer.rb"),
File.join(root_directory, SERIALIZERS_TEST_DIR, "%MODEL_NAME%_serializer_spec.rb"),
File.join(root_directory, SERIALIZERS_SPEC_DIR, "%MODEL_NAME%_serializer_spec.rb")
]
end
def files_by_pattern(root_directory, pattern_type)
case pattern_type
when 'test' then test_files(root_directory)
when 'fixture' then fixture_files(root_directory)
when 'scaffold' then scaffold_files(root_directory)
when 'factory' then factory_files(root_directory)
when 'serializer' then serialize_files(root_directory)
when 'controller'
[File.join(root_directory, CONTROLLER_DIR, "%PLURALIZED_MODEL_NAME%_controller.rb")]
when 'admin'
[File.join(root_directory, ACTIVEADMIN_DIR, "%MODEL_NAME%.rb")]
when 'helper'
[File.join(root_directory, HELPER_DIR, "%PLURALIZED_MODEL_NAME%_helper.rb")]
else
[]
end
end
def get_patterns(pattern_types=[])
current_patterns = []
root_dir.each do |root_directory|
Array(pattern_types).each do |pattern_type|
current_patterns += files_by_pattern(root_directory, pattern_type)
end
end
current_patterns.map{ |p| p.sub(/^[\/]*/, '') }
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
# BigDecimals need to be output in a non-normalized form and quoted.
when BigDecimal then value.to_s('F')
when Array then value.map {|v| quote(v)}
else
value.inspect
end
end
def schema_default(klass, column)
quote(klass.column_defaults[column.name])
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"
info<< "#\n"
if options[:format_markdown]
info<< "# Table name: `#{klass.table_name}`\n"
info<< "#\n"
info<< "# ### Columns\n"
else
info<< "# Table name: #{klass.table_name}\n"
end
info<< "#\n"
max_size = klass.column_names.map(&:size).max || 0
max_size += options[:format_rdoc] ? 5 : 1
md_names_overhead = 6
md_type_allowance = 18
bare_type_allowance = 16
if options[:format_markdown]
info<< sprintf( "# %-#{max_size + md_names_overhead}.#{max_size + md_names_overhead}s | %-#{md_type_allowance}.#{md_type_allowance}s | %s\n", 'Name', 'Type', 'Attributes' )
info<< "# #{ '-' * ( max_size + md_names_overhead ) } | #{'-' * md_type_allowance} | #{ '-' * 27 }\n"
end
cols = if ignore_columns = options[:ignore_columns]
klass.columns.reject do |col|
col.name.match(/#{ignore_columns}/)
end
else
klass.columns
end
cols = cols.sort_by(&:name) if options[:sort]
cols = classified_sort(cols) if options[:classified_sort]
cols.each do |col|
col_type = (col.type || col.sql_type).to_s
attrs = []
attrs << "default(#{schema_default(klass, col)})" unless col.default.nil? || NO_DEFAULT_COL_TYPES.include?(col_type)
attrs << 'not null' unless col.null
attrs << 'primary key' if klass.primary_key && (klass.primary_key.is_a?(Array) ? klass.primary_key.collect(&:to_sym).include?(col.name.to_sym) : col.name.to_sym == klass.primary_key.to_sym)
if col_type == 'decimal'
col_type << "(#{col.precision}, #{col.scale})"
elsif col_type != 'spatial'
if col.limit
if col.limit.is_a? Array
attrs << "(#{col.limit.join(', ')})"
else
col_type << "(#{col.limit})" unless hide_limit?(col_type, options)
end
end
end
# Check out if we got an array column
attrs << 'is an Array' if col.respond_to?(:array) && col.array
# 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}"
elsif col.respond_to?(:geometric_type) && col.geometric_type.present?
attrs << "#{col.geometric_type.to_s.downcase}, #{col.srid}"
end
# Check if the column has indices and print "indexed" if true
# 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.sort_by(&:name).each do |ind|
ind = ind.columns.reject! { |i| i == col.name }
attrs << (ind.empty? ? "indexed" : "indexed => [#{ind.join(", ")}]")
end
end
end
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]
name_remainder = max_size - col.name.length
type_remainder = (md_type_allowance - 2) - col_type.length
info << (sprintf("# **`%s`**%#{name_remainder}s | `%s`%#{type_remainder}s | `%s`", col.name, " ", col_type, " ", attrs.join(", ").rstrip)).gsub('``', ' ').rstrip + "\n"
else
info << sprintf("# %-#{max_size}.#{max_size}s:%-#{bare_type_allowance}.#{bare_type_allowance}s %s", col.name, col_type, attrs.join(", ")).rstrip + "\n"
end
end
if options[:show_indexes] && klass.table_exists?
info << get_index_info(klass, options)
end
if options[:show_foreign_keys] && klass.table_exists?
info << get_foreign_key_info(klass, options)
end
if options[:format_rdoc]
info << "#--\n"
info << "# #{END_MARK}\n"
info << "#++\n"
else
info << "#\n"
end
end
def get_index_info(klass, options={})
if options[:format_markdown]
index_info = "#\n# ### Indexes\n#\n"
else
index_info = "#\n# Indexes\n#\n"
end
indexes = klass.connection.indexes(klass.table_name)
return '' if indexes.empty?
max_size = indexes.collect{|index| index.name.size}.max + 1
indexes.sort_by(&:name).each do |index|
if options[:format_markdown]
index_info << sprintf("# * `%s`%s:\n# * **`%s`**\n", index.name, index.unique ? " (_unique_)" : "", index.columns.join("`**\n# * **`"))
else
index_info << sprintf("# %-#{max_size}.#{max_size}s %s %s", index.name, "(#{index.columns.join(",")})", index.unique ? "UNIQUE" : "").rstrip + "\n"
end
end
index_info
end
def hide_limit?(col_type, options)
excludes =
if options[:hide_limit_column_types].blank?
NO_LIMIT_COL_TYPES
else
options[:hide_limit_column_types].split(',')
end
excludes.include?(col_type)
end
def get_foreign_key_info(klass, options={})
if options[:format_markdown]
fk_info = "#\n# ### Foreign Keys\n#\n"
else
fk_info = "#\n# Foreign Keys\n#\n"
end
return '' unless klass.connection.respond_to?(:supports_foreign_keys?) &&
klass.connection.supports_foreign_keys? && klass.connection.respond_to?(:foreign_keys)
foreign_keys = klass.connection.foreign_keys(klass.table_name)
return '' if foreign_keys.empty?
max_size = foreign_keys.collect{|fk| fk.name.size}.max + 1
foreign_keys.sort_by(&:name).each do |fk|
ref_info = "#{fk.column} => #{fk.to_table}.#{fk.primary_key}"
constraints_info = ''
constraints_info += "ON DELETE => #{fk.on_delete} " if fk.on_delete
constraints_info += "ON UPDATE => #{fk.on_update} " if fk.on_update
constraints_info.strip!
if options[:format_markdown]
fk_info << sprintf("# * `%s`%s:\n# * **`%s`**\n", fk.name, constraints_info.blank? ? '' : " (_#{constraints_info}_)", ref_info)
else
fk_info << sprintf("# %-#{max_size}.#{max_size}s %s %s", fk.name, "(#{ref_info})", constraints_info).rstrip + "\n"
end
end
fk_info
end
# Add a schema block to a file. If the file already contains
# a schema info block (a comment starting with "== Schema Information"), check if it
# matches the block that is already there. If so, leave it be. If not, remove the old
# info block and write a new one.
#
# == Returns:
# true or false depending on whether the file was modified.
#
# === Options (opts)
# :force<Symbol>:: whether to update the file even if it doesn't seem to need it.
# :position_in_*<Symbol>:: where to place the annotated section in fixture or model file,
# :before, :top, :after or :bottom. Default is :before.
#
def annotate_one_file(file_name, info_block, position, 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]?)/
old_header = old_content.match(header_pattern).to_s
new_header = info_block.match(header_pattern).to_s
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
magic_comment_matcher= Regexp.new(/(^#\s*encoding:.*\n)|(^# coding:.*\n)|(^# -\*- coding:.*\n)|(^# -\*- encoding\s?:.*\n)|(^#\s*frozen_string_literal:.+\n)|(^# -\*- frozen_string_literal\s*:.+-\*-\n)/)
magic_comments= old_content.scan(magic_comment_matcher).flatten.compact
if old_columns == new_columns && !options[:force]
return false
else
# Replace inline the old schema info with the new schema info
new_content = old_content.sub(annotate_pattern(options), info_block + "\n")
if new_content.end_with?(info_block + "\n")
new_content = old_content.sub(annotate_pattern(options), "\n" + info_block)
end
wrapper_open = options[:wrapper_open] ? "# #{options[:wrapper_open]}\n" : ""
wrapper_close = options[:wrapper_close] ? "# #{options[:wrapper_close]}\n" : ""
wrapped_info_block = "#{wrapper_open}#{info_block}#{wrapper_close}"
# if there *was* no old schema info (no substitution happened) or :force was passed,
# we simply need to insert it in correct position
if new_content == old_content || options[:force]
old_content.sub!(magic_comment_matcher, '')
old_content.sub!(annotate_pattern(options), '')
if %w(after bottom).include?(options[position].to_s)
new_content = magic_comments.join + (old_content.rstrip + "\n\n" + wrapped_info_block)
else
new_content = magic_comments.join + wrapped_info_block + "\n" + old_content
end
end
File.open(file_name, 'wb') { |f| f.puts new_content }
return true
end
else
false
end
end
def remove_annotation_of_file(file_name, options={})
if File.exist?(file_name)
content = File.read(file_name)
wrapper_open = options[:wrapper_open] ? "# #{options[:wrapper_open]}\n" : ""
content.sub!(/(#{wrapper_open})?#{annotate_pattern(options)}/, '')
File.open(file_name, 'wb') { |f| f.puts content }
true
else
false
end
end
def matched_types(options)
types = MATCHED_TYPES
types << 'admin' if options[:active_admin] =~ TRUE_RE && !types.include?('admin')
types
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.
#
# === Options (opts)
# :position_in_class<Symbol>:: where to place the annotated section in model file
# :position_in_test<Symbol>:: where to place the annotated section in test/spec file(s)
# :position_in_fixture<Symbol>:: where to place the annotated section in fixture file
# :position_in_factory<Symbol>:: where to place the annotated section in factory file
# :position_in_serializer<Symbol>:: where to place the annotated section in serializer file
# :exclude_tests<Symbol>:: whether to skip modification of test/spec files
# :exclude_fixtures<Symbol>:: whether to skip modification of fixture files
# :exclude_factories<Symbol>:: whether to skip modification of factory files
# :exclude_serializers<Symbol>:: whether to skip modification of serializer files
# :exclude_scaffolds<Symbol>:: whether to skip modification of scaffold files
# :exclude_controllers<Symbol>:: whether to skip modification of controller files
# :exclude_helpers<Symbol>:: whether to skip modification of helper files
#
# == Returns:
# an array of file names that were annotated.
#
def annotate(klass, file, header, options={})
begin
klass.reset_column_information
info = get_schema_info(klass, header, options)
model_name = klass.name.underscore
table_name = klass.table_name
model_file_name = File.join(file)
annotated = []
if annotate_one_file(model_file_name, info, :position_in_class, options_with_position(options, :position_in_class))
annotated << model_file_name
end
matched_types(options).each do |key|
exclusion_key = "exclude_#{key.pluralize}".to_sym
position_key = "position_in_#{key}".to_sym
# Same options for active_admin models
if key == 'admin'
exclusion_key = 'exclude_class'.to_sym
position_key = 'position_in_class'.to_sym
end
unless options[exclusion_key]
self.get_patterns(key).
map { |f| resolve_filename(f, model_name, table_name) }.
each { |f|
if annotate_one_file(f, info, position_key, options_with_position(options, position_key))
annotated << f
end
}
end
end
rescue Exception => e
puts "Unable to annotate #{file}: #{e.message}"
puts "\t" + e.backtrace.join("\n\t") if options[:trace]
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 the path
# of model files from root dir. Otherwise we take all the model files
# in the model_dir directory.
def get_model_files(options)
models = []
if !options[:is_rake]
models = ARGV.dup.reject{|m| m.match(/^(.*)=/)}
end
if models.empty?
begin
model_dir.each do |dir|
Dir.chdir(dir) do
lst =
if options[:ignore_model_sub_dir]
Dir["*.rb"].map{ |f| [dir, f] }
else
Dir["**/*.rb"].reject{ |f| f["concerns/"] }.map{ |f| [dir, f] }
end
models.concat(lst)
end
end
rescue SystemCallError
puts "No models found in directory '#{model_dir.join("', '")}'."
puts "Either specify models on the command line, or use the --model-dir option."
puts "Call 'annotate --help' for more info."
exit 1
end
end
models
end
# 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)
model_path = file.gsub(/\.rb$/, '')
model_dir.each { |dir| model_path = model_path.gsub(/^#{dir}/, '').gsub(/^\//, '') }
begin
get_loaded_model(model_path) || raise(BadModelFileError.new)
rescue LoadError
# this is for non-rails projects, which don't get Rails auto-require magic
file_path = File.expand_path(file)
if File.file?(file_path) && silence_warnings { Kernel.require(file_path) }
retry
elsif model_path =~ /\//
model_path = model_path.split('/')[1..-1].join('/').to_s
retry
else
raise
end
end
end
# Retrieve loaded model class by path to the file where it's supposed to be defined.
def get_loaded_model(model_path)
begin
ActiveSupport::Inflector.constantize(ActiveSupport::Inflector.camelize(model_path))
rescue
# Revert to the old way but it is not really robust
ObjectSpace.each_object(::Class).
select do |c|
Class === c && # note: we use === to avoid a bug in activesupport 2.3.14 OptionMerger vs. is_a?
c.ancestors.respond_to?(:include?) && # to fix FactoryGirl bug, see https://github.com/ctran/annotate_models/pull/82
c.ancestors.include?(ActiveRecord::Base)
end.
detect { |c| ActiveSupport::Inflector.underscore(c.to_s) == model_path }
end
end
def parse_options(options={})
self.model_dir = options[:model_dir] if options[:model_dir]
self.root_dir = options[:root_dir] if options[:root_dir]
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={})
parse_options(options)
header = options[:format_markdown] ? PREFIX_MD.dup : PREFIX.dup
version = ActiveRecord::Migrator.current_version rescue 0
if options[:include_version] && version > 0
header << "\n# Schema version: #{version}"
end
annotated = []
get_model_files(options).each do |path, filename|
annotate_model_file(annotated, filename.nil? ? File.join(path) : File.join(path, filename), header, options)
end
if annotated.empty?
puts 'Model files unchanged.'
else
puts "Annotated (#{annotated.length}): #{annotated.join(', ')}"
end
end
def annotate_model_file(annotated, file, header, options)
begin
return false if /# -\*- SkipSchemaAnnotations.*/ =~ (File.exist?(file) ? File.read(file) : '')
klass = get_model_class(file)
if klass && klass < ActiveRecord::Base && !klass.abstract_class? && klass.table_exists?
annotated.concat(annotate(klass, file, header, options))
end
rescue BadModelFileError => e
unless options[:ignore_unknown_models]
puts "Unable to annotate #{file}: #{e.message}"
puts "\t" + e.backtrace.join("\n\t") if options[:trace]
end
rescue Exception => e
puts "Unable to annotate #{file}: #{e.message}"
puts "\t" + e.backtrace.join("\n\t") if options[:trace]
end
end
def remove_annotations(options={})
parse_options(options)
deannotated = []
deannotated_klass = false
get_model_files(options).each do |file|
file = File.join(file)
begin
klass = get_model_class(file)
if klass < ActiveRecord::Base && !klass.abstract_class?
model_name = klass.name.underscore
table_name = klass.table_name
model_file_name = file
deannotated_klass = true if remove_annotation_of_file(model_file_name, options)
get_patterns(matched_types(options)).
map { |f| resolve_filename(f, model_name, table_name) }.
each do |f|
if File.exist?(f)
remove_annotation_of_file(f, options)
deannotated_klass = true
end
end
end
deannotated << klass if deannotated_klass
rescue Exception => e
puts "Unable to deannotate #{File.join(file)}: #{e.message}"
puts "\t" + e.backtrace.join("\n\t") if options[:trace]
end
end
puts "Removed annotations from: #{deannotated.join(', ')}"
end
def resolve_filename(filename_template, model_name, table_name)
filename_template.
gsub('%MODEL_NAME%', model_name).
gsub('%PLURALIZED_MODEL_NAME%', model_name.pluralize).
gsub('%TABLE_NAME%', table_name || model_name.pluralize)
end
def classified_sort(cols)
rest_cols = []
timestamps = []
associations = []
id = nil
cols.each do |c|
if c.name.eql?('id')
id = c
elsif c.name.eql?('created_at') || c.name.eql?('updated_at')
timestamps << c
elsif c.name[-3,3].eql?('_id')
associations << c
else
rest_cols << c
end
end
[rest_cols, timestamps, associations].each {|a| a.sort_by!(&:name) }
return ([id] << rest_cols << timestamps << associations).flatten.compact
end
# Ignore warnings for the duration of the block ()
def silence_warnings
old_verbose, $VERBOSE = $VERBOSE, nil
yield
ensure
$VERBOSE = old_verbose
end
end
class BadModelFileError < LoadError
def to_s
"file doesn't contain a valid model class"
end
end
end
# == Annotate Routes
#
# Based on:
#
#
#
# Prepends the output of "rake routes" to the top of your routes.rb file.
# Yes, it's simple but I'm thick and often need a reminder of what my routes
# mean.
#
# Running this task will replace any exising route comment generated by the
# task. Best to back up your routes file before running:
#
# Author:
# Gavin Montague
# gavin@leftbrained.co.uk
#
# Released under the same license as Ruby. No Support. No Warranty.
#
module AnnotateRoutes
PREFIX = '# == Route Map'
def self.do_annotations(options={})
return unless routes_exists?
routes_map = AnnotateRoutes.app_routes_map(options)
header = [
"#{PREFIX}" + (options[:timestamp] ? " (Updated #{Time.now.strftime('%Y-%m-%d %H:%M')})" : ''), '#'
] + routes_map.map { |line| "# #{line}".rstrip }
existing_text = File.read(routes_file)
if write_contents(existing_text, header, options)
puts "#{routes_file} annotated."
end
end
def self.remove_annotations(options={})
return unless routes_exists?
existing_text = File.read(routes_file)
content, where_header_found = strip_annotations(existing_text)
content = strip_on_removal(content, where_header_found)
if write_contents(existing_text, content, options)
puts "Removed annotations from #{routes_file}."
end
end
private
def self.app_routes_map(options)
routes_map = `rake routes`.split(/\n/, -1)
# In old versions of Rake, the first line of output was the cwd. Not so
# much in newer ones. We ditch that line if it exists, and if not, we
# keep the line around.
routes_map.shift if routes_map.first =~ /^\(in \//
# Skip routes which match given regex
# Note: it matches the complete line (route_name, path, controller/action)
if options[:ignore_routes]
routes_map.reject! { |line| line =~ /#{options[:ignore_routes]}/ }
end
routes_map
end
def self.routes_file
@routes_rb ||= File.join('config', 'routes.rb')
end
def self.routes_exists?
routes_exists = File.exists?(routes_file)
puts "Can't find routes.rb" unless routes_exists
routes_exists
end
def self.write_contents(existing_text, header, options = {})
content, where_header_found = strip_annotations(existing_text)
new_content = annotate_routes(header, content, where_header_found, options)
# Make sure we end on a trailing newline.
new_content << '' unless new_content.last == ''
new_text = new_content.join("\n")
if existing_text == new_text
puts "#{routes_file} unchanged."
false
else
File.open(routes_file, 'wb') { |f| f.puts(new_text) }
true
end
end
def self.annotate_routes(header, content, where_header_found, options = {})
if %w(before top).include?(options[:position_in_routes])
header = header << '' if content.first != ''
new_content = header + content
else
# Ensure we have adequate trailing newlines at the end of the file to
# ensure a blank line separating the content from the annotation.
content << '' unless content.last == ''
# We're moving something from the top of the file to the bottom, so ditch
# the spacer we put in the first time around.
content.shift if where_header_found == :before && content.first == ''
new_content = content + header
end
new_content
end
# TODO: write the method doc using ruby rdoc formats
# where_header_found => This will either be :before, :after, or
# a number. If the number is > 0, the
# annotation was found somewhere in the
# middle of the file. If the number is
# zero, no annotation was found.
def self.strip_annotations(content)
real_content = []
mode = :content
header_found_at = 0
content.split(/\n/, -1).each_with_index do |line, line_number|
if mode == :header && line !~ /\s*#/
mode = :content
next unless line == ''
elsif mode == :content
if line =~ /^\s*#\s*== Route.*$/
header_found_at = line_number + 1 # index start's at 0
mode = :header
else
real_content << line
end
end
end
where_header_found(real_content, header_found_at)
end
def self.where_header_found(real_content, header_found_at)
# By default assume the annotation was found in the middle of the file
# ... unless we have evidence it was at the beginning ...
return real_content, :before if header_found_at == 1
# ... or that it was at the end.
return real_content, :after if header_found_at >= real_content.count
# and the default
return real_content, header_found_at
end
def self.strip_on_removal(content, where_header_found)
if where_header_found == :before
content.shift while content.first == ''
elsif where_header_found == :after
content.pop while content.last == ''
end
# TODO: If the user buried it in the middle, we should probably see about
# TODO: preserving a single line of space between the content above and
# TODO: below...
content
end
end
require 'rubygems'
require 'rake'
# Make tasks visible for Rails also when used as gem.
Dir[File.join(File.dirname(__FILE__), '..', 'tasks', '**/*.rake')].each { |rake| load rake }
Dir[File.join(File.dirname(__FILE__), '..', '..', 'tasks', '**/*.rake')].each { |rake| load rake }
module Annotate
def self.version
'2.7.1'
end
end
Add a .rake file that automatically annotates models when you do a db:migrate
in development mode:
rails generate annotate:install
module Annotate
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
# 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
# You can override any of these by setting an environment variable of the
# same name.
Annotate.set_defaults(
'routes' => 'false',
'position_in_routes' => 'before',
'position_in_class' => 'before',
'position_in_test' => 'before',
'position_in_fixture' => 'before',
'position_in_factory' => 'before',
'position_in_serializer' => 'before',
'show_foreign_keys' => 'true',
'show_indexes' => 'true',
'simple_indexes' => 'false',
'model_dir' => 'app/models',
'root_dir' => '',
'include_version' => 'false',
'require' => '',
'exclude_tests' => 'false',
'exclude_fixtures' => 'false',
'exclude_factories' => 'false',
'exclude_serializers' => 'false',
'exclude_scaffolds' => 'true',
'exclude_controllers' => 'true',
'exclude_helpers' => 'true',
'ignore_model_sub_dir' => 'false',
'ignore_columns' => nil,
'ignore_routes' => nil,
'ignore_unknown_models' => 'false',
'hide_limit_column_types' => '<%= AnnotateModels::NO_LIMIT_COL_TYPES.join(",") %>',
'skip_on_db_migrate' => 'false',
'format_bare' => 'true',
'format_rdoc' => 'false',
'format_markdown' => 'false',
'sort' => 'false',
'force' => 'false',
'trace' => 'false',
'wrapper_open' => nil,
'wrapper_close' => nil
)
end
Annotate.load_tasks
end
annotate_lib = File.expand_path(File.dirname(File.dirname(__FILE__)))
unless 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"
options={is_rake: true}
ENV['position'] = options[:position] = Annotate.fallback(ENV['position'], 'before')
options[:position_in_class] = Annotate.fallback(ENV['position_in_class'], ENV['position'])
options[:position_in_fixture] = Annotate.fallback(ENV['position_in_fixture'], ENV['position'])
options[:position_in_factory] = Annotate.fallback(ENV['position_in_factory'], ENV['position'])
options[:position_in_test] = Annotate.fallback(ENV['position_in_test'], ENV['position'])
options[:position_in_serializer] = Annotate.fallback(ENV['position_in_serializer'], ENV['position'])
options[:show_foreign_keys] = Annotate.true?(ENV['show_foreign_keys'])
options[:show_indexes] = Annotate.true?(ENV['show_indexes'])
options[:simple_indexes] = Annotate.true?(ENV['simple_indexes'])
options[:model_dir] = ENV['model_dir'] ? ENV['model_dir'].split(',') : ['app/models']
options[:root_dir] = ENV['root_dir'] ? ENV['root_dir'].split(',') : ['']
options[:include_version] = Annotate.true?(ENV['include_version'])
options[:require] = ENV['require'] ? ENV['require'].split(',') : []
options[:exclude_tests] = Annotate.true?(ENV['exclude_tests'])
options[:exclude_factories] = Annotate.true?(ENV['exclude_factories'])
options[:exclude_fixtures] = Annotate.true?(ENV['exclude_fixtures'])
options[:exclude_serializers] = Annotate.true?(ENV['exclude_serializers'])
options[:exclude_scaffolds] = Annotate.true?(ENV['exclude_scaffolds'])
options[:exclude_controllers] = Annotate.true?(ENV.fetch('exclude_controllers', 'true'))
options[:exclude_helpers] = Annotate.true?(ENV.fetch('exclude_helpers', 'true'))
options[:ignore_model_sub_dir] = Annotate.true?(ENV['ignore_model_sub_dir'])
options[:format_bare] = Annotate.true?(ENV['format_bare'])
options[:format_rdoc] = Annotate.true?(ENV['format_rdoc'])
options[:format_markdown] = Annotate.true?(ENV['format_markdown'])
options[:sort] = Annotate.true?(ENV['sort'])
options[:force] = Annotate.true?(ENV['force'])
options[:classified_sort] = Annotate.true?(ENV['classified_sort'])
options[:trace] = Annotate.true?(ENV['trace'])
options[:wrapper_open] = Annotate.fallback(ENV['wrapper_open'], ENV['wrapper'])
options[:wrapper_close] = Annotate.fallback(ENV['wrapper_close'], ENV['wrapper'])
options[:ignore_columns] = ENV.fetch('ignore_columns', nil)
options[:ignore_routes] = ENV.fetch('ignore_routes', nil)
options[:hide_limit_column_types] = Annotate.fallback(ENV['hide_limit_column_types'], '')
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"
require "#{annotate_lib}/annotate/active_record_patch"
options={is_rake: true}
options[:model_dir] = ENV['model_dir']
options[:root_dir] = ENV['root_dir']
options[:require] = ENV['require'] ? ENV['require'].split(',') : []
options[:trace] = Annotate.true?(ENV['trace'])
AnnotateModels.remove_annotations(options)
end
desc "Adds the route map to routes.rb"
task :annotate_routes => :environment do
annotate_lib = File.expand_path(File.dirname(File.dirname(__FILE__)))
require "#{annotate_lib}/annotate/annotate_routes"
options={}
ENV['position'] = options[:position] = Annotate.fallback(ENV['position'], 'before')
options[:position_in_routes] = Annotate.fallback(ENV['position_in_routes'], ENV['position'])
options[:ignore_routes] = Annotate.fallback(ENV['ignore_routes'], nil)
options[:require] = ENV['require'] ? ENV['require'].split(',') : []
AnnotateRoutes.do_annotations(options)
end
desc "Removes the route map from routes.rb"
task :remove_routes => :environment do
annotate_lib = File.expand_path(File.dirname(File.dirname(__FILE__)))
require "#{annotate_lib}/annotate/annotate_routes"
options={}
options[:require] = ENV['require'] ? ENV['require'].split(',') : []
AnnotateRoutes.remove_annotations(options)
end
# These tasks are added to the project if you install annotate as a Rails plugin.
# (They are not used to build annotate itself.)
# Append annotations to Rake tasks for ActiveRecord, so annotate automatically gets
# run after doing db:migrate.
namespace :db do
[:migrate, :rollback].each do |cmd|
task cmd do
Rake::Task['set_annotation_options'].invoke
Annotate::Migration.update_annotations
end
namespace cmd do
[:change, :up, :down, :reset, :redo].each do |t|
task t do
Rake::Task['set_annotation_options'].invoke
Annotate::Migration.update_annotations
end
end
end
end
end
module Annotate
class Migration
@@working = false
def self.update_annotations
unless @@working || Annotate.skip_on_migration?
@@working = true
self.update_models if Annotate.include_models?
self.update_routes if Annotate.include_routes?
end
end
def self.update_models
if Rake::Task.task_defined?("annotate_models")
Rake::Task["annotate_models"].invoke
elsif Rake::Task.task_defined?("app:annotate_models")
Rake::Task["app:annotate_models"].invoke
end
end
def self.update_routes
if Rake::Task.task_defined?("annotate_routes")
Rake::Task["annotate_routes"].invoke
end
end
end
end
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