Commit 89bce57f by cuong.tran

Merge branch 'release/v2.7.3'

parents 5f37a974 db3eb14c
# This configuration was generated by # This configuration was generated by
# `rubocop --auto-gen-config` # `rubocop --auto-gen-config`
# on 2016-12-17 10:16:05 +0100 using RuboCop version 0.46.0. # on 2017-10-13 10:01:18 +0200 using RuboCop version 0.46.0.
# The point is for the user to remove these configuration records # The point is for the user to remove these configuration records
# one by one as the offenses are removed from the code base. # one by one as the offenses are removed from the code base.
# Note that changes in the inspected code, or installation of new # Note that changes in the inspected code, or installation of new
...@@ -65,11 +65,6 @@ Lint/HandleExceptions: ...@@ -65,11 +65,6 @@ Lint/HandleExceptions:
Exclude: Exclude:
- 'bin/annotate' - 'bin/annotate'
# Offense count: 8
Lint/IneffectiveAccessModifier:
Exclude:
- 'lib/annotate/annotate_routes.rb'
# Offense count: 1 # Offense count: 1
# Cop supports --auto-correct. # Cop supports --auto-correct.
# Configuration parameters: EnforcedStyle, SupportedStyles. # Configuration parameters: EnforcedStyle, SupportedStyles.
...@@ -88,62 +83,51 @@ Lint/ShadowingOuterLocalVariable: ...@@ -88,62 +83,51 @@ Lint/ShadowingOuterLocalVariable:
Exclude: Exclude:
- 'Rakefile' - 'Rakefile'
# Offense count: 6 # Offense count: 7
# Cop supports --auto-correct. # Cop supports --auto-correct.
# Configuration parameters: IgnoreEmptyBlocks, AllowUnusedKeywordArguments. # Configuration parameters: IgnoreEmptyBlocks, AllowUnusedKeywordArguments.
Lint/UnusedBlockArgument: Lint/UnusedBlockArgument:
Exclude: Exclude:
- 'bin/annotate' - 'bin/annotate'
# Offense count: 1 # Offense count: 18
# Configuration parameters: ContextCreatingMethods.
Lint/UselessAccessModifier:
Exclude:
- 'lib/annotate/annotate_routes.rb'
# Offense count: 17
Metrics/AbcSize: Metrics/AbcSize:
Max: 159 Max: 142
# Offense count: 3 # Offense count: 3
# Configuration parameters: CountComments. # Configuration parameters: CountComments.
Metrics/BlockLength: Metrics/BlockLength:
Max: 134 Max: 142
# Offense count: 2 # Offense count: 2
Metrics/BlockNesting: Metrics/BlockNesting:
Max: 4 Max: 4
# Offense count: 8 # Offense count: 9
Metrics/CyclomaticComplexity: Metrics/CyclomaticComplexity:
Max: 42 Max: 36
# Offense count: 350 # Offense count: 380
# Configuration parameters: AllowHeredoc, AllowURI, URISchemes, IgnoreCopDirectives, IgnoredPatterns. # Configuration parameters: AllowHeredoc, AllowURI, URISchemes, IgnoreCopDirectives, IgnoredPatterns.
# URISchemes: http, https # URISchemes: http, https
Metrics/LineLength: Metrics/LineLength:
Max: 543 Max: 276
# Offense count: 24 # Offense count: 26
# Configuration parameters: CountComments. # Configuration parameters: CountComments.
Metrics/MethodLength: Metrics/MethodLength:
Max: 79 Max: 75
# Offense count: 1
# Configuration parameters: CountComments.
Metrics/ModuleLength:
Max: 116
# Offense count: 7 # Offense count: 7
Metrics/PerceivedComplexity: Metrics/PerceivedComplexity:
Max: 48 Max: 42
# Offense count: 1 # Offense count: 1
Style/AccessorMethodName: Style/AccessorMethodName:
Exclude: Exclude:
- 'lib/annotate.rb' - 'lib/annotate.rb'
# Offense count: 3 # Offense count: 2
# Cop supports --auto-correct. # Cop supports --auto-correct.
Style/AlignArray: Style/AlignArray:
Exclude: Exclude:
...@@ -259,7 +243,7 @@ Style/ExtraSpacing: ...@@ -259,7 +243,7 @@ Style/ExtraSpacing:
- 'spec/integration/rails_4.2.0/Gemfile' - 'spec/integration/rails_4.2.0/Gemfile'
- 'spec/integration/rails_4.2.0/config.ru' - 'spec/integration/rails_4.2.0/config.ru'
# Offense count: 9 # Offense count: 10
# Configuration parameters: EnforcedStyle, SupportedStyles. # Configuration parameters: EnforcedStyle, SupportedStyles.
# SupportedStyles: format, sprintf, percent # SupportedStyles: format, sprintf, percent
Style/FormatString: Style/FormatString:
...@@ -267,13 +251,6 @@ Style/FormatString: ...@@ -267,13 +251,6 @@ Style/FormatString:
- 'lib/annotate/annotate_models.rb' - 'lib/annotate/annotate_models.rb'
- 'lib/annotate/annotate_routes.rb' - 'lib/annotate/annotate_routes.rb'
# Offense count: 181
# Cop supports --auto-correct.
# Configuration parameters: EnforcedStyle, SupportedStyles.
# SupportedStyles: when_needed, always
Style/FrozenStringLiteralComment:
Enabled: false
# Offense count: 7 # Offense count: 7
# Configuration parameters: MinBodyLength. # Configuration parameters: MinBodyLength.
Style/GuardClause: Style/GuardClause:
...@@ -356,16 +333,6 @@ Style/NumericLiterals: ...@@ -356,16 +333,6 @@ Style/NumericLiterals:
# Offense count: 2 # Offense count: 2
# Cop supports --auto-correct. # Cop supports --auto-correct.
# Configuration parameters: AutoCorrect, EnforcedStyle, SupportedStyles.
# SupportedStyles: predicate, comparison
Style/NumericPredicate:
Exclude:
- 'spec/**/*'
- 'lib/annotate.rb'
- 'lib/annotate/annotate_models.rb'
# Offense count: 6
# Cop supports --auto-correct.
# Configuration parameters: PreferredDelimiters. # Configuration parameters: PreferredDelimiters.
Style/PercentLiteralDelimiters: Style/PercentLiteralDelimiters:
Exclude: Exclude:
...@@ -458,16 +425,15 @@ Style/SpaceAroundKeyword: ...@@ -458,16 +425,15 @@ Style/SpaceAroundKeyword:
- 'spec/integration/rails_4.2.0/Gemfile' - 'spec/integration/rails_4.2.0/Gemfile'
- 'spec/integration/standalone/Gemfile' - 'spec/integration/standalone/Gemfile'
# Offense count: 6 # Offense count: 4
# Cop supports --auto-correct. # Cop supports --auto-correct.
# Configuration parameters: AllowForAlignment. # Configuration parameters: AllowForAlignment.
Style/SpaceAroundOperators: Style/SpaceAroundOperators:
Exclude: Exclude:
- 'lib/annotate/annotate_models.rb' - 'lib/annotate/annotate_models.rb'
- 'lib/tasks/annotate_models.rake'
- 'lib/tasks/annotate_routes.rake' - 'lib/tasks/annotate_routes.rake'
# Offense count: 4 # Offense count: 1
# Cop supports --auto-correct. # Cop supports --auto-correct.
# Configuration parameters: EnforcedStyle, SupportedStyles. # Configuration parameters: EnforcedStyle, SupportedStyles.
# SupportedStyles: space, no_space # SupportedStyles: space, no_space
...@@ -517,14 +483,14 @@ Style/SpaceInsideStringInterpolation: ...@@ -517,14 +483,14 @@ Style/SpaceInsideStringInterpolation:
Exclude: Exclude:
- 'lib/annotate/annotate_models.rb' - 'lib/annotate/annotate_models.rb'
# Offense count: 223 # Offense count: 237
# Cop supports --auto-correct. # Cop supports --auto-correct.
# Configuration parameters: EnforcedStyle, SupportedStyles, ConsistentQuotesInMultiline. # Configuration parameters: EnforcedStyle, SupportedStyles, ConsistentQuotesInMultiline.
# SupportedStyles: single_quotes, double_quotes # SupportedStyles: single_quotes, double_quotes
Style/StringLiterals: Style/StringLiterals:
Enabled: false Enabled: false
# Offense count: 2 # Offense count: 1
# Cop supports --auto-correct. # Cop supports --auto-correct.
# Configuration parameters: EnforcedStyle, SupportedStyles. # Configuration parameters: EnforcedStyle, SupportedStyles.
# SupportedStyles: single_quotes, double_quotes # SupportedStyles: single_quotes, double_quotes
...@@ -581,7 +547,7 @@ Style/UnneededInterpolation: ...@@ -581,7 +547,7 @@ Style/UnneededInterpolation:
Exclude: Exclude:
- 'bin/annotate' - 'bin/annotate'
# Offense count: 8 # Offense count: 4
# Cop supports --auto-correct. # Cop supports --auto-correct.
Style/UnneededPercentQ: Style/UnneededPercentQ:
Exclude: Exclude:
......
sudo: false sudo: false
language: ruby language: ruby
rvm: rvm:
- 1.9.3 - 2.2.7
- 2.0 - 2.3.4
- 2.1 - 2.4.1
- 2.2.5 - ruby-head
- 2.3.0
- 2.4.0
- ruby-head
matrix: matrix:
allow_failures: allow_failures:
- rvm: ruby-head - rvm: ruby-head
- rvm: 1.9.3
before_install: before_install:
- gem update --system - gem update --system
- gem update bundler - gem update bundler
script: script:
- bundle exec rubocop && bundle exec rspec - bundle exec rubocop && bundle exec rspec
deploy:
provider: rubygems
api_key:
secure: Y7DUitak26kcRAAkgph/7m6Y1wHeObD0BelSSJbmCfjkRd/qaVy7fz9VvHL9zxlRJtLGVHInyCnwcfzinibY6OFd3MoMYHKv8GFa2LxLJNEVSY46KQYFxfH5JTg1ejh6ldoJRRBoeOx9dcWS80pRNjYMKPGnpSz7yDBl1azibFs=
gem: annotate
on:
tags: true
repo: ctran/annotate_models
== 2.7.3
See https://github.com/ctran/annotate_models/releases/tag/v2.7.3
== 2.7.2 == 2.7.2
See https://github.com/ctran/annotate_models/releases/tag/v2.7.2 See https://github.com/ctran/annotate_models/releases/tag/v2.7.2
......
source 'https://rubygems.org' source 'https://rubygems.org'
ruby '>= 2.2.0'
gem 'activerecord', '>= 4.2.5', require: false gem 'activerecord', '>= 4.2.5', require: false
gem 'rake', require: false gem 'rake', require: false
group :development do group :development do
gem 'bump' gem 'bump'
gem 'mg', require: false gem 'mg', require: false
gem 'travis', require: false
platforms :mri, :mingw do platforms :mri, :mingw do
gem 'yard', require: false gem 'yard', require: false
end end
......
...@@ -17,7 +17,7 @@ your... ...@@ -17,7 +17,7 @@ your...
- Object Daddy exemplars - Object Daddy exemplars
- Machinist blueprints - Machinist blueprints
- Fabrication fabricators - Fabrication fabricators
- Thoughtbot's factory_girl factories, i.e. the (spec|test)/factories/<model>_factory.rb files - Thoughtbot's factory_bot factories, i.e. the (spec|test)/factories/<model>_factory.rb files
- routes.rb file (for Rails projects) - routes.rb file (for Rails projects)
The schema comment looks like this: The schema comment looks like this:
...@@ -55,11 +55,15 @@ Also, if you pass the -r option, it'll annotate routes.rb with the output of ...@@ -55,11 +55,15 @@ Also, if you pass the -r option, it'll annotate routes.rb with the output of
Into Gemfile from rubygems.org: Into Gemfile from rubygems.org:
group :development do
gem 'annotate' gem 'annotate'
end
Into Gemfile from Github: Into Gemfile from Github:
group :development do
gem 'annotate', git: 'https://github.com/ctran/annotate_models.git' gem 'annotate', git: 'https://github.com/ctran/annotate_models.git'
end
Into environment gems from rubygems.org: Into environment gems from rubygems.org:
...@@ -154,14 +158,15 @@ If you want to run <code>rake db:migrate</code> as a one-off without running ann ...@@ -154,14 +158,15 @@ If you want to run <code>rake db:migrate</code> as a one-off without running ann
you can do so with a simple environment variable, instead of editing the you can do so with a simple environment variable, instead of editing the
+.rake+ file: +.rake+ file:
skip_on_db_migrate=1 rake db:migrate ANNOTATE_SKIP_ON_DB_MIGRATE=1 rake db:migrate
== Options == Options
Usage: annotate [options] [model_file]* Usage: annotate [options] [model_file]*
-d, --delete Remove annotations from all model files or the routes.rb 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) -p [before|top|after|bottom], Place the annotations at the top (before) or the bottom (after) of the model/test/fixture/factory/route/serializer file(s)
--position
--pc, --position-in-class [before|top|after|bottom] --pc, --position-in-class [before|top|after|bottom]
Place the annotations at the top (before) or the bottom (after) of the model file Place the annotations at the top (before) or the bottom (after) of the model file
--pf, --position-in-factory [before|top|after|bottom] --pf, --position-in-factory [before|top|after|bottom]
...@@ -179,15 +184,19 @@ you can do so with a simple environment variable, instead of editing the ...@@ -179,15 +184,19 @@ you can do so with a simple environment variable, instead of editing the
--wo, --wrapper-open STR Annotation wrapper opening. --wo, --wrapper-open STR Annotation wrapper opening.
--wc, --wrapper-close STR Annotation wrapper closing --wc, --wrapper-close STR Annotation wrapper closing
-r, --routes Annotate routes.rb with the output of 'rake routes' -r, --routes Annotate routes.rb with the output of 'rake routes'
-aa, --active-admin Annotate all activeadmin models -a, --active-admin Annotate active_admin models
-v, --version Show the current version of this gem -v, --version Show the current version of this gem
-m, --show-migration Include the migration version number in the annotation -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 -k, --show-foreign-keys List the table's foreign key constraints in the annotation
--ck, --complete-foreign-keys
Complete foreign key names 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 -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 commas --model-dir dir Annotate model files stored in dir rather than app/models, separate multiple dirs with commas
--root-dir dir Annotate files stored within root dir projects, separate multiple dirs with commas
--ignore-model-subdirects Ignore subdirectories of the models directory --ignore-model-subdirects Ignore subdirectories of the models directory
--sort Sort columns alphabetically, rather than in creation order --sort Sort columns alphabetically, rather than in creation order
--classified-sort Sort columns alphabetically, but first goes id, then the rest columns, then the timestamp columns and then the association columns
-R, --require path Additional file to require before loading models, may be used multiple times -R, --require path Additional file to require before loading models, may be used multiple times
-e [tests,fixtures,factories,serializers], -e [tests,fixtures,factories,serializers],
--exclude Do not annotate fixtures, test files, factories, and/or serializers --exclude Do not annotate fixtures, test files, factories, and/or serializers
...@@ -197,6 +206,13 @@ you can do so with a simple environment variable, instead of editing the ...@@ -197,6 +206,13 @@ you can do so with a simple environment variable, instead of editing the
--timestamp Include timestamp in (routes) annotation --timestamp Include timestamp in (routes) annotation
--trace If unable to annotate a file, print the full stack trace, not just the exception message. --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)'` -I, --ignore-columns REGEX don't annotate columns that match a given REGEX (i.e., `annotate -I '^(id|updated_at|created_at)'`
--ignore-routes REGEX don't annotate routes that match a given REGEX (i.e., `annotate -I '(mobile|resque|pghero)'`
--hide-limit-column-types VALUES
don't show limit for given column types, separated by commas (i.e., `integer,boolean,text`)
--hide-default-column-types VALUES
don't show default for given column types, separated by commas (i.e., `json,jsonb,hstore`)
--ignore-unknown-models don't display warnings for bad model files
--with-comment include database comments in model annotations
...@@ -242,7 +258,7 @@ extra carefully, and consider using one. ...@@ -242,7 +258,7 @@ extra carefully, and consider using one.
== Links == Links
- Factory Girl: http://github.com/thoughtbot/factory_girl - Factory Bot: http://github.com/thoughtbot/factory_bot
- Object Daddy: http://github.com/flogic/object_daddy - Object Daddy: http://github.com/flogic/object_daddy
- Machinist: http://github.com/notahat/machinist - Machinist: http://github.com/notahat/machinist
- Fabrication: http://github.com/paulelliott/fabrication - Fabrication: http://github.com/paulelliott/fabrication
......
...@@ -28,7 +28,7 @@ require 'mg' ...@@ -28,7 +28,7 @@ require 'mg'
begin begin
MG.new('annotate.gemspec') MG.new('annotate.gemspec')
rescue Exception rescue Exception
STDERR.puts("WARNING: Couldn't read gemspec. As such, a number of tasks may be unavailable to you until you run 'rake gem:gemspec' to correct the issue.") $stderr.puts("WARNING: Couldn't read gemspec. As such, a number of tasks may be unavailable to you until you run 'rake gem:gemspec' to correct the issue.")
# Gemspec is probably in a broken state, so let's give ourselves a chance to # Gemspec is probably in a broken state, so let's give ourselves a chance to
# build a new one... # build a new one...
end end
......
...@@ -7,7 +7,7 @@ Gem::Specification.new do |s| ...@@ -7,7 +7,7 @@ Gem::Specification.new do |s|
s.name = 'annotate' s.name = 'annotate'
s.version = Annotate.version s.version = Annotate.version
s.required_ruby_version = '>= 1.9.3' s.required_ruby_version = '>= 2.2.0'
s.required_rubygems_version = Gem::Requirement.new('>= 0') if s.respond_to? :required_rubygems_version= 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.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.description = 'Annotates Rails/ActiveRecord Models, routes, fixtures, and others based on the database schema.'
......
...@@ -90,7 +90,7 @@ OptionParser.new do |opts| ...@@ -90,7 +90,7 @@ OptionParser.new do |opts|
ENV['routes'] = 'true' ENV['routes'] = 'true'
end end
opts.on('-aa', '--active-admin', 'Annotate active_admin models') do opts.on('-a', '--active-admin', 'Annotate active_admin models') do
ENV['active_admin'] = 'true' ENV['active_admin'] = 'true'
end end
...@@ -107,6 +107,12 @@ OptionParser.new do |opts| ...@@ -107,6 +107,12 @@ OptionParser.new do |opts|
ENV['show_foreign_keys'] = 'yes' ENV['show_foreign_keys'] = 'yes'
end end
opts.on('--ck',
'--complete-foreign-keys', 'Complete foreign key names in the annotation') do
ENV['show_foreign_keys'] = 'yes'
ENV['show_complete_foreign_keys'] = 'yes'
end
opts.on('-i', '--show-indexes', opts.on('-i', '--show-indexes',
"List the table's database indexes in the annotation") do "List the table's database indexes in the annotation") do
ENV['show_indexes'] = 'yes' ENV['show_indexes'] = 'yes'
...@@ -191,12 +197,16 @@ OptionParser.new do |opts| ...@@ -191,12 +197,16 @@ OptionParser.new do |opts|
opts.on('--ignore-unknown-models', "don't display warnings for bad model files") do |values| opts.on('--ignore-unknown-models', "don't display warnings for bad model files") do |values|
ENV['ignore_unknown_models'] = 'true' ENV['ignore_unknown_models'] = 'true'
end end
opts.on('--with-comment', "include database comments in model annotations") do |values|
ENV['with_comment'] = 'true'
end
end.parse! end.parse!
options = Annotate.setup_options( options = Annotate.setup_options(
is_rake: ENV['is_rake'] && !ENV['is_rake'].empty? is_rake: ENV['is_rake'] && !ENV['is_rake'].empty?
) )
Annotate.eager_load(options) Annotate.eager_load(options) if Annotate.include_models?
AnnotateModels.send(target_action, options) if Annotate.include_models? AnnotateModels.send(target_action, options) if Annotate.include_models?
AnnotateRoutes.send(target_action, options) if Annotate.include_routes? AnnotateRoutes.send(target_action, options) if Annotate.include_routes?
...@@ -30,9 +30,10 @@ module Annotate ...@@ -30,9 +30,10 @@ module Annotate
:show_indexes, :simple_indexes, :include_version, :exclude_tests, :show_indexes, :simple_indexes, :include_version, :exclude_tests,
:exclude_fixtures, :exclude_factories, :ignore_model_sub_dir, :exclude_fixtures, :exclude_factories, :ignore_model_sub_dir,
:format_bare, :format_rdoc, :format_markdown, :sort, :force, :trace, :format_bare, :format_rdoc, :format_markdown, :sort, :force, :trace,
:timestamp, :exclude_serializers, :classified_sort, :show_foreign_keys, :timestamp, :exclude_serializers, :classified_sort,
:show_foreign_keys, :show_complete_foreign_keys,
:exclude_scaffolds, :exclude_controllers, :exclude_helpers, :exclude_scaffolds, :exclude_controllers, :exclude_helpers,
:exclude_sti_subclasses, :ignore_unknown_models :exclude_sti_subclasses, :ignore_unknown_models, :with_comment
].freeze ].freeze
OTHER_OPTIONS = [ OTHER_OPTIONS = [
:ignore_columns, :skip_on_db_migrate, :wrapper_open, :wrapper_close, :ignore_columns, :skip_on_db_migrate, :wrapper_open, :wrapper_close,
...@@ -105,7 +106,7 @@ module Annotate ...@@ -105,7 +106,7 @@ module Annotate
end end
def self.skip_on_migration? def self.skip_on_migration?
ENV['skip_on_db_migrate'] =~ TRUE_RE ENV['ANNOTATE_SKIP_ON_DB_MIGRATE'] =~ TRUE_RE || ENV['skip_on_db_migrate'] =~ TRUE_RE
end end
def self.include_routes? def self.include_routes?
...@@ -113,7 +114,7 @@ module Annotate ...@@ -113,7 +114,7 @@ module Annotate
end end
def self.include_models? def self.include_models?
true ENV['routes'] !~ TRUE_RE
end end
def self.loaded_tasks=(val) def self.loaded_tasks=(val)
...@@ -168,7 +169,7 @@ module Annotate ...@@ -168,7 +169,7 @@ module Annotate
require 'rake/dsl_definition' require 'rake/dsl_definition'
rescue StandardError => e rescue StandardError => e
# We might just be on an old version of Rake... # We might just be on an old version of Rake...
puts e.message $stderr.puts e.message
exit e.status_code exit e.status_code
end end
require 'rake' require 'rake'
......
...@@ -12,6 +12,8 @@ module AnnotateModels ...@@ -12,6 +12,8 @@ module AnnotateModels
PREFIX_MD = '## Schema Information'.freeze PREFIX_MD = '## Schema Information'.freeze
END_MARK = '== Schema Information End'.freeze END_MARK = '== Schema Information End'.freeze
SKIP_ANNOTATION_PREFIX = '# -\*- SkipSchemaAnnotations'.freeze
MATCHED_TYPES = %w(test fixture factory serializer scaffold controller helper).freeze MATCHED_TYPES = %w(test fixture factory serializer scaffold controller helper).freeze
# File.join for windows reverse bar compat? # File.join for windows reverse bar compat?
...@@ -65,12 +67,27 @@ module AnnotateModels ...@@ -65,12 +67,27 @@ module AnnotateModels
# Don't show default value for these column types # Don't show default value for these column types
NO_DEFAULT_COL_TYPES = %w(json jsonb hstore).freeze NO_DEFAULT_COL_TYPES = %w(json jsonb hstore).freeze
INDEX_CLAUSES = {
unique: {
default: 'UNIQUE',
markdown: '_unique_'
},
where: {
default: 'WHERE',
markdown: '_where_'
},
using: {
default: 'USING',
markdown: '_using_'
}
}.freeze
class << self class << self
def annotate_pattern(options = {}) def annotate_pattern(options = {})
if options[:wrapper_open] 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*/ return /(?:^(\n|\r\n)?# (?:#{options[:wrapper_open]}).*(\n|\r\n)?# (?:#{COMPAT_PREFIX}|#{COMPAT_PREFIX_MD}).*?(\n|\r\n)(#.*(\n|\r\n))*(\n|\r\n)*)|^(\n|\r\n)?# (?:#{COMPAT_PREFIX}|#{COMPAT_PREFIX_MD}).*?(\n|\r\n)(#.*(\n|\r\n))*(\n|\r\n)*/
end end
/^\n?# (?:#{COMPAT_PREFIX}|#{COMPAT_PREFIX_MD}).*?\n(#.*\n)*\n*/ /^(\n|\r\n)?# (?:#{COMPAT_PREFIX}|#{COMPAT_PREFIX_MD}).*?(\n|\r\n)(#.*(\n|\r\n))*(\n|\r\n)*/
end end
def model_dir def model_dir
...@@ -127,6 +144,8 @@ module AnnotateModels ...@@ -127,6 +144,8 @@ module AnnotateModels
File.join(root_directory, FACTORY_GIRL_SPEC_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_TEST_DIR, "%TABLE_NAME%.rb"), # (new style)
File.join(root_directory, FACTORY_GIRL_SPEC_DIR, "%TABLE_NAME%.rb"), # (new style) File.join(root_directory, FACTORY_GIRL_SPEC_DIR, "%TABLE_NAME%.rb"), # (new style)
File.join(root_directory, FACTORY_GIRL_TEST_DIR, "%PLURALIZED_MODEL_NAME%.rb"), # (new style)
File.join(root_directory, FACTORY_GIRL_SPEC_DIR, "%PLURALIZED_MODEL_NAME%.rb"), # (new style)
File.join(root_directory, FABRICATORS_TEST_DIR, "%MODEL_NAME%_fabricator.rb"), File.join(root_directory, FABRICATORS_TEST_DIR, "%MODEL_NAME%_fabricator.rb"),
File.join(root_directory, FABRICATORS_SPEC_DIR, "%MODEL_NAME%_fabricator.rb") File.join(root_directory, FABRICATORS_SPEC_DIR, "%MODEL_NAME%_fabricator.rb")
] ]
...@@ -195,8 +214,8 @@ module AnnotateModels ...@@ -195,8 +214,8 @@ module AnnotateModels
return indexes if indexes.any? || !klass.table_name_prefix return indexes if indexes.any? || !klass.table_name_prefix
# Try to search the table without prefix # Try to search the table without prefix
table_name.to_s.slice!(klass.table_name_prefix) table_name_without_prefix = table_name.to_s.sub(klass.table_name_prefix, '')
klass.connection.indexes(table_name) klass.connection.indexes(table_name_without_prefix)
end end
# Use the column information in an ActiveRecord class # Use the column information in an ActiveRecord class
...@@ -207,11 +226,7 @@ module AnnotateModels ...@@ -207,11 +226,7 @@ module AnnotateModels
info = "# #{header}\n" info = "# #{header}\n"
info << get_schema_header_text(klass, options) info << get_schema_header_text(klass, options)
max_size = klass.column_names.map(&:size).max || 0 max_size = max_schema_info_width(klass, options)
with_comment = options[:with_comment] && klass.columns.first.respond_to?(:comment)
max_size = klass.columns.map{|col| col.name.size + col.comment.size }.max || 0 if with_comment
max_size += 2 if with_comment
max_size += options[:format_rdoc] ? 5 : 1
md_names_overhead = 6 md_names_overhead = 6
md_type_allowance = 18 md_type_allowance = 18
bare_type_allowance = 16 bare_type_allowance = 16
...@@ -232,7 +247,7 @@ module AnnotateModels ...@@ -232,7 +247,7 @@ module AnnotateModels
cols = cols.sort_by(&:name) if options[:sort] cols = cols.sort_by(&:name) if options[:sort]
cols = classified_sort(cols) if options[:classified_sort] cols = classified_sort(cols) if options[:classified_sort]
cols.each do |col| cols.each do |col|
col_type = (col.type || col.sql_type).to_s col_type = get_col_type(col)
attrs = [] attrs = []
attrs << "default(#{schema_default(klass, col)})" unless col.default.nil? || hide_default?(col_type, options) attrs << "default(#{schema_default(klass, col)})" unless col.default.nil? || hide_default?(col_type, options)
attrs << 'unsigned' if col.respond_to?(:unsigned?) && col.unsigned? attrs << 'unsigned' if col.respond_to?(:unsigned?) && col.unsigned?
...@@ -274,7 +289,7 @@ module AnnotateModels ...@@ -274,7 +289,7 @@ module AnnotateModels
end end
end end
end end
col_name = if with_comment col_name = if with_comments?(klass, options) && col.comment
"#{col.name}(#{col.comment})" "#{col.name}(#{col.comment})"
else else
col.name col.name
...@@ -337,15 +352,83 @@ module AnnotateModels ...@@ -337,15 +352,83 @@ module AnnotateModels
max_size = indexes.collect{|index| index.name.size}.max + 1 max_size = indexes.collect{|index| index.name.size}.max + 1
indexes.sort_by(&:name).each do |index| indexes.sort_by(&:name).each do |index|
index_info << if options[:format_markdown] index_info << if options[:format_markdown]
sprintf("# * `%s`%s:\n# * **`%s`**\n", index.name, index.unique ? " (_unique_)" : "", Array(index.columns).join("`**\n# * **`")) final_index_string_in_markdown(index)
else else
sprintf("# %-#{max_size}.#{max_size}s %s %s", index.name, "(#{Array(index.columns).join(",")})", index.unique ? "UNIQUE" : "").rstrip + "\n" final_index_string(index, max_size)
end end
end end
index_info index_info
end end
def get_col_type(col)
if col.respond_to?(:bigint?) && col.bigint?
'bigint'
else
(col.type || col.sql_type).to_s
end
end
def index_columns_info(index)
Array(index.columns).map do |col|
if index.try(:orders) && index.orders[col.to_s]
"#{col} #{index.orders[col.to_s].upcase}"
else
col.to_s.gsub("\r", '\r').gsub("\n", '\n')
end
end
end
def index_unique_info(index, format = :default)
index.unique ? " #{INDEX_CLAUSES[:unique][format]}" : ''
end
def index_where_info(index, format = :default)
value = index.try(:where).try(:to_s)
if value.blank?
''
else
" #{INDEX_CLAUSES[:where][format]} #{value}"
end
end
def index_using_info(index, format = :default)
value = index.try(:using) && index.using.try(:to_sym)
if !value.blank? && value != :btree
" #{INDEX_CLAUSES[:using][format]} #{value}"
else
''
end
end
def final_index_string_in_markdown(index)
details = sprintf(
"%s%s%s",
index_unique_info(index, :markdown),
index_where_info(index, :markdown),
index_using_info(index, :markdown)
).strip
details = " (#{details})" unless details.blank?
sprintf(
"# * `%s`%s:\n# * **`%s`**\n",
index.name,
details,
index_columns_info(index).join("`**\n# * **`")
)
end
def final_index_string(index, max_size)
sprintf(
"# %-#{max_size}.#{max_size}s %s%s%s%s",
index.name,
"(#{index_columns_info(index).join(',')})",
index_unique_info(index),
index_where_info(index),
index_using_info(index)
).rstrip + "\n"
end
def hide_limit?(col_type, options) def hide_limit?(col_type, options)
excludes = excludes =
if options[:hide_limit_column_types].blank? if options[:hide_limit_column_types].blank?
...@@ -417,7 +500,7 @@ module AnnotateModels ...@@ -417,7 +500,7 @@ module AnnotateModels
def annotate_one_file(file_name, info_block, position, options = {}) def annotate_one_file(file_name, info_block, position, options = {})
if File.exist?(file_name) if File.exist?(file_name)
old_content = File.read(file_name) old_content = File.read(file_name)
return false if old_content =~ /# -\*- SkipSchemaAnnotations.*\n/ return false if old_content =~ /#{SKIP_ANNOTATION_PREFIX}.*\n/
# Ignore the Schema version line because it changes with each migration # Ignore the Schema version line because it changes with each migration
header_pattern = /(^# Table name:.*?\n(#.*[\r]?\n)*[\r]?)/ header_pattern = /(^# Table name:.*?\n(#.*[\r]?\n)*[\r]?)/
...@@ -428,8 +511,7 @@ module AnnotateModels ...@@ -428,8 +511,7 @@ module AnnotateModels
old_columns = old_header && old_header.scan(column_pattern).sort old_columns = old_header && old_header.scan(column_pattern).sort
new_columns = new_header && new_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_block = magic_comments_as_string(old_content)
magic_comments = old_content.scan(magic_comment_matcher).flatten.compact
if old_columns == new_columns && !options[:force] if old_columns == new_columns && !options[:force]
return false return false
...@@ -447,13 +529,13 @@ module AnnotateModels ...@@ -447,13 +529,13 @@ module AnnotateModels
# if there *was* no old schema info (no substitution happened) or :force was passed, # if there *was* no old schema info (no substitution happened) or :force was passed,
# we simply need to insert it in correct position # we simply need to insert it in correct position
if new_content == old_content || options[:force] if new_content == old_content || options[:force]
old_content.sub!(magic_comment_matcher, '') old_content.gsub!(magic_comment_matcher, '')
old_content.sub!(annotate_pattern(options), '') old_content.sub!(annotate_pattern(options), '')
new_content = if %w(after bottom).include?(options[position].to_s) new_content = if %w(after bottom).include?(options[position].to_s)
magic_comments.join + (old_content.rstrip + "\n\n" + wrapped_info_block) magic_comments_block + (old_content.rstrip + "\n\n" + wrapped_info_block)
else else
magic_comments.join + wrapped_info_block + "\n" + old_content magic_comments_block + wrapped_info_block + "\n" + old_content
end end
end end
...@@ -465,9 +547,25 @@ module AnnotateModels ...@@ -465,9 +547,25 @@ module AnnotateModels
end end
end end
def magic_comment_matcher
Regexp.new(/(^#\s*encoding:.*(?:\n|r\n))|(^# coding:.*(?:\n|\r\n))|(^# -\*- coding:.*(?:\n|\r\n))|(^# -\*- encoding\s?:.*(?:\n|\r\n))|(^#\s*frozen_string_literal:.+(?:\n|\r\n))|(^# -\*- frozen_string_literal\s*:.+-\*-(?:\n|\r\n))/)
end
def magic_comments_as_string(content)
magic_comments = content.scan(magic_comment_matcher).flatten.compact
if magic_comments.any?
magic_comments.join + "\n"
else
''
end
end
def remove_annotation_of_file(file_name, options = {}) def remove_annotation_of_file(file_name, options = {})
if File.exist?(file_name) if File.exist?(file_name)
content = File.read(file_name) content = File.read(file_name)
return false if content =~ /#{SKIP_ANNOTATION_PREFIX}.*\n/
wrapper_open = options[:wrapper_open] ? "# #{options[:wrapper_open]}\n" : '' wrapper_open = options[:wrapper_open] ? "# #{options[:wrapper_open]}\n" : ''
content.sub!(/(#{wrapper_open})?#{annotate_pattern(options)}/, '') content.sub!(/(#{wrapper_open})?#{annotate_pattern(options)}/, '')
...@@ -542,8 +640,8 @@ module AnnotateModels ...@@ -542,8 +640,8 @@ module AnnotateModels
end end
end end
rescue StandardError => e rescue StandardError => e
puts "Unable to annotate #{file}: #{e.message}" $stderr.puts "Unable to annotate #{file}: #{e.message}"
puts "\t" + e.backtrace.join("\n\t") if options[:trace] $stderr.puts "\t" + e.backtrace.join("\n\t") if options[:trace]
end end
annotated annotated
...@@ -559,34 +657,52 @@ module AnnotateModels ...@@ -559,34 +657,52 @@ module AnnotateModels
# of model files from root dir. Otherwise we take all the model files # of model files from root dir. Otherwise we take all the model files
# in the model_dir directory. # in the model_dir directory.
def get_model_files(options) def get_model_files(options)
models = [] model_files = []
unless options[:is_rake]
models = ARGV.dup.reject { |m| m.match(/^(.*)=/) } model_files = list_model_files_from_argument unless options[:is_rake]
end
return model_files unless model_files.empty?
if models.empty?
begin
model_dir.each do |dir| model_dir.each do |dir|
Dir.chdir(dir) do Dir.chdir(dir) do
lst = list = if options[:ignore_model_sub_dir]
if options[:ignore_model_sub_dir] Dir["*.rb"].map { |f| [dir, f] }
Dir["*.rb"].map{ |f| [dir, f] }
else else
Dir["**/*.rb"].reject{ |f| f["concerns/"] }.map{ |f| [dir, f] } Dir["**/*.rb"].reject { |f| f["concerns/"] }.map { |f| [dir, f] }
end end
models.concat(lst) model_files.concat(list)
end end
end end
model_files
rescue SystemCallError rescue SystemCallError
puts "No models found in directory '#{model_dir.join("', '")}'." $stderr.puts "No models found in directory '#{model_dir.join("', '")}'."
puts "Either specify models on the command line, or use the --model-dir option." $stderr.puts "Either specify models on the command line, or use the --model-dir option."
puts "Call 'annotate --help' for more info." $stderr.puts "Call 'annotate --help' for more info."
exit 1 exit 1
end end
def list_model_files_from_argument
return [] if ARGV.empty?
specified_files = ARGV.map { |file| File.expand_path(file) }
model_files = model_dir.flat_map do |dir|
absolute_dir_path = File.expand_path(dir)
specified_files
.find_all { |file| file.start_with?(absolute_dir_path) }
.map { |file| [dir, file.sub("#{absolute_dir_path}/", '')] }
end
if model_files.size != specified_files.size
puts "The specified file could not be found in directory '#{model_dir.join("', '")}'."
puts "Call 'annotate --help' for more info."
exit 1
end end
models model_files
end end
private :list_model_files_from_argument
# Retrieve the classes belonging to the model names we're asked to process # Retrieve the classes belonging to the model names we're asked to process
# Check for namespaced models in subdirectories as well as models # Check for namespaced models in subdirectories as well as models
...@@ -595,11 +711,11 @@ module AnnotateModels ...@@ -595,11 +711,11 @@ module AnnotateModels
model_path = file.gsub(/\.rb$/, '') model_path = file.gsub(/\.rb$/, '')
model_dir.each { |dir| model_path = model_path.gsub(/^#{dir}/, '').gsub(/^\//, '') } model_dir.each { |dir| model_path = model_path.gsub(/^#{dir}/, '').gsub(/^\//, '') }
begin begin
get_loaded_model(model_path) || raise(BadModelFileError.new) get_loaded_model(model_path, file) || raise(BadModelFileError.new)
rescue LoadError rescue LoadError
# this is for non-rails projects, which don't get Rails auto-require magic # this is for non-rails projects, which don't get Rails auto-require magic
file_path = File.expand_path(file) file_path = File.expand_path(file)
if File.file?(file_path) && silence_warnings { Kernel.require(file_path) } if File.file?(file_path) && Kernel.require(file_path)
retry retry
elsif model_path =~ /\// elsif model_path =~ /\//
model_path = model_path.split('/')[1..-1].join('/').to_s model_path = model_path.split('/')[1..-1].join('/').to_s
...@@ -610,10 +726,26 @@ module AnnotateModels ...@@ -610,10 +726,26 @@ module AnnotateModels
end end
end end
# Retrieve loaded model class
def get_loaded_model(model_path, file)
loaded_model_class = get_loaded_model_by_path(model_path)
return loaded_model_class if loaded_model_class
# We cannot get loaded model when `model_path` is loaded by Rails
# auto_load/eager_load paths. Try all possible model paths one by one.
absolute_file = File.expand_path(file)
model_paths =
$LOAD_PATH.select { |path| absolute_file.include?(path) }
.map { |path| absolute_file.sub(path, '').sub(/\.rb$/, '').sub(/^\//, '') }
model_paths
.map { |path| get_loaded_model_by_path(path) }
.find { |loaded_model| !loaded_model.nil? }
end
# Retrieve loaded model class by path to the file where it's supposed to be defined. # Retrieve loaded model class by path to the file where it's supposed to be defined.
def get_loaded_model(model_path) def get_loaded_model_by_path(model_path)
ActiveSupport::Inflector.constantize(ActiveSupport::Inflector.camelize(model_path)) ActiveSupport::Inflector.constantize(ActiveSupport::Inflector.camelize(model_path))
rescue rescue StandardError, LoadError
# Revert to the old way but it is not really robust # Revert to the old way but it is not really robust
ObjectSpace.each_object(::Class) ObjectSpace.each_object(::Class)
.select do |c| .select do |c|
...@@ -624,10 +756,15 @@ module AnnotateModels ...@@ -624,10 +756,15 @@ module AnnotateModels
end end
def parse_options(options = {}) def parse_options(options = {})
self.model_dir = options[:model_dir] if options[:model_dir] self.model_dir = split_model_dir(options[:model_dir]) if options[:model_dir]
self.root_dir = options[:root_dir] if options[:root_dir] self.root_dir = options[:root_dir] if options[:root_dir]
end end
def split_model_dir(option_value)
option_value = option_value.is_a?(Array) ? option_value : option_value.split(',')
option_value.map(&:strip).reject(&:empty?)
end
# We're passed a name of things that might be # We're passed a name of things that might be
# ActiveRecord models. If we can find the class, and # ActiveRecord models. If we can find the class, and
# if its a subclass of ActiveRecord::Base, # if its a subclass of ActiveRecord::Base,
...@@ -655,7 +792,7 @@ module AnnotateModels ...@@ -655,7 +792,7 @@ module AnnotateModels
def annotate_model_file(annotated, file, header, options) def annotate_model_file(annotated, file, header, options)
begin begin
return false if /# -\*- SkipSchemaAnnotations.*/ =~ (File.exist?(file) ? File.read(file) : '') return false if /#{SKIP_ANNOTATION_PREFIX}.*/ =~ (File.exist?(file) ? File.read(file) : '')
klass = get_model_class(file) klass = get_model_class(file)
do_annotate = klass && do_annotate = klass &&
klass < ActiveRecord::Base && klass < ActiveRecord::Base &&
...@@ -666,12 +803,12 @@ module AnnotateModels ...@@ -666,12 +803,12 @@ module AnnotateModels
annotated.concat(annotate(klass, file, header, options)) if do_annotate annotated.concat(annotate(klass, file, header, options)) if do_annotate
rescue BadModelFileError => e rescue BadModelFileError => e
unless options[:ignore_unknown_models] unless options[:ignore_unknown_models]
puts "Unable to annotate #{file}: #{e.message}" $stderr.puts "Unable to annotate #{file}: #{e.message}"
puts "\t" + e.backtrace.join("\n\t") if options[:trace] $stderr.puts "\t" + e.backtrace.join("\n\t") if options[:trace]
end end
rescue StandardError => e rescue StandardError => e
puts "Unable to annotate #{file}: #{e.message}" $stderr.puts "Unable to annotate #{file}: #{e.message}"
puts "\t" + e.backtrace.join("\n\t") if options[:trace] $stderr.puts "\t" + e.backtrace.join("\n\t") if options[:trace]
end end
end end
...@@ -701,8 +838,8 @@ module AnnotateModels ...@@ -701,8 +838,8 @@ module AnnotateModels
end end
deannotated << klass if deannotated_klass deannotated << klass if deannotated_klass
rescue StandardError => e rescue StandardError => e
puts "Unable to deannotate #{File.join(file)}: #{e.message}" $stderr.puts "Unable to deannotate #{File.join(file)}: #{e.message}"
puts "\t" + e.backtrace.join("\n\t") if options[:trace] $stderr.puts "\t" + e.backtrace.join("\n\t") if options[:trace]
end end
end end
puts "Removed annotations from: #{deannotated.join(', ')}" puts "Removed annotations from: #{deannotated.join(', ')}"
...@@ -737,13 +874,26 @@ module AnnotateModels ...@@ -737,13 +874,26 @@ module AnnotateModels
([id] << rest_cols << timestamps << associations).flatten.compact ([id] << rest_cols << timestamps << associations).flatten.compact
end end
# Ignore warnings for the duration of the block () private
def silence_warnings
old_verbose = $VERBOSE def with_comments?(klass, options)
$VERBOSE = nil options[:with_comment] &&
yield klass.columns.first.respond_to?(:comment) &&
ensure klass.columns.any? { |col| !col.comment.nil? }
$VERBOSE = old_verbose end
def max_schema_info_width(klass, options)
if with_comments?(klass, options)
max_size = klass.columns.map do |column|
column.name.size + (column.comment ? column.comment.size : 0)
end.max || 0
max_size += 2
else
max_size = klass.column_names.map(&:size).max
end
max_size += options[:format_rdoc] ? 5 : 1
max_size
end end
end end
......
# rubocop:disable Metrics/ModuleLength
# == Annotate Routes # == Annotate Routes
# #
# Based on: # Based on:
...@@ -36,7 +38,18 @@ module AnnotateRoutes ...@@ -36,7 +38,18 @@ module AnnotateRoutes
def header(options = {}) def header(options = {})
routes_map = app_routes_map(options) routes_map = app_routes_map(options)
out = ["# #{options[:format_markdown] ? PREFIX_MD : PREFIX}" + (options[:timestamp] ? " (Updated #{Time.now.strftime('%Y-%m-%d %H:%M')})" : '')] magic_comments_map, routes_map = extract_magic_comments_from_array(routes_map)
out = []
magic_comments_map.each do |magic_comment|
out << magic_comment
end
out << '' if magic_comments_map.any?
out += ["# #{options[:wrapper_open]}"] if options[:wrapper_open]
out += ["# #{options[:format_markdown] ? PREFIX_MD : PREFIX}" + (options[:timestamp] ? " (Updated #{Time.now.strftime('%Y-%m-%d %H:%M')})" : '')]
out += ['#'] out += ['#']
return out if routes_map.size.zero? return out if routes_map.size.zero?
...@@ -51,35 +64,56 @@ module AnnotateRoutes ...@@ -51,35 +64,56 @@ module AnnotateRoutes
out += ["# #{content(routes_map[0], maxs, options)}"] out += ["# #{content(routes_map[0], maxs, options)}"]
end end
out + routes_map[1..-1].map { |line| "# #{content(options[:format_markdown] ? line.split(' ') : line, maxs, options)}" } out += routes_map[1..-1].map { |line| "# #{content(options[:format_markdown] ? line.split(' ') : line, maxs, options)}" }
out += ["# #{options[:wrapper_close]}"] if options[:wrapper_close]
out
end end
def do_annotations(options = {}) def do_annotations(options = {})
return unless routes_exists? return unless routes_exists?
existing_text = File.read(routes_file) existing_text = File.read(routes_file)
if write_contents(existing_text, header(options), options) if rewrite_contents_with_header(existing_text, header(options), options)
puts "#{routes_file} annotated." puts "#{routes_file} annotated."
end end
end end
def remove_annotations(options={}) def remove_annotations(_options={})
return unless routes_exists? return unless routes_exists?
existing_text = File.read(routes_file) existing_text = File.read(routes_file)
content, where_header_found = strip_annotations(existing_text) content, where_header_found = strip_annotations(existing_text)
new_content = strip_on_removal(content, where_header_found)
content = strip_on_removal(content, where_header_found) if rewrite_contents(existing_text, new_content)
if write_contents(existing_text, content, options)
puts "Removed annotations from #{routes_file}." puts "Removed annotations from #{routes_file}."
end end
end end
end end
private def self.magic_comment_matcher
Regexp.new(/(^#\s*encoding:.*)|(^# coding:.*)|(^# -\*- coding:.*)|(^# -\*- encoding\s?:.*)|(^#\s*frozen_string_literal:.+)|(^# -\*- frozen_string_literal\s*:.+-\*-)/)
end
# @param [Array<String>] content
# @return [Array<String>] all found magic comments
# @return [Array<String>] content without magic comments
def self.extract_magic_comments_from_array(content_array)
magic_comments = []
new_content = []
content_array.map do |row|
if row =~ magic_comment_matcher
magic_comments << row.strip
else
new_content << row
end
end
[magic_comments, new_content]
end
def self.app_routes_map(options) def self.app_routes_map(options)
routes_map = `rake routes`.split(/\n/, -1) routes_map = `rake routes`.chomp("\n").split(/\n/, -1)
# In old versions of Rake, the first line of output was the cwd. Not so # 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 # much in newer ones. We ditch that line if it exists, and if not, we
...@@ -106,7 +140,22 @@ module AnnotateRoutes ...@@ -106,7 +140,22 @@ module AnnotateRoutes
routes_exists routes_exists
end end
def self.write_contents(existing_text, header, options = {}) # @param [String, Array<String>]
def self.rewrite_contents(existing_text, new_content)
# 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.rewrite_contents_with_header(existing_text, header, options = {})
content, where_header_found = strip_annotations(existing_text) content, where_header_found = strip_annotations(existing_text)
new_content = annotate_routes(header, content, where_header_found, options) new_content = annotate_routes(header, content, where_header_found, options)
...@@ -124,9 +173,11 @@ module AnnotateRoutes ...@@ -124,9 +173,11 @@ module AnnotateRoutes
end end
def self.annotate_routes(header, content, where_header_found, options = {}) def self.annotate_routes(header, content, where_header_found, options = {})
magic_comments_map, content = extract_magic_comments_from_array(content)
if %w(before top).include?(options[:position_in_routes]) if %w(before top).include?(options[:position_in_routes])
header = header << '' if content.first != '' header = header << '' if content.first != ''
new_content = header + content magic_comments_map << '' if magic_comments_map.any?
new_content = magic_comments_map + header + content
else else
# Ensure we have adequate trailing newlines at the end of the file to # Ensure we have adequate trailing newlines at the end of the file to
# ensure a blank line separating the content from the annotation. # ensure a blank line separating the content from the annotation.
...@@ -136,7 +187,7 @@ module AnnotateRoutes ...@@ -136,7 +187,7 @@ module AnnotateRoutes
# the spacer we put in the first time around. # the spacer we put in the first time around.
content.shift if where_header_found == :before && content.first == '' content.shift if where_header_found == :before && content.first == ''
new_content = content + header new_content = magic_comments_map + content + header
end end
new_content new_content
...@@ -156,7 +207,7 @@ module AnnotateRoutes ...@@ -156,7 +207,7 @@ module AnnotateRoutes
content.split(/\n/, -1).each_with_index do |line, line_number| content.split(/\n/, -1).each_with_index do |line, line_number|
if mode == :header && line !~ /\s*#/ if mode == :header && line !~ /\s*#/
mode = :content mode = :content
next unless line == '' real_content << line unless line.blank?
elsif mode == :content elsif mode == :content
if line =~ /^\s*#\s*== Route.*$/ if line =~ /^\s*#\s*== Route.*$/
header_found_at = line_number + 1 # index start's at 0 header_found_at = line_number + 1 # index start's at 0
......
module Annotate module Annotate
def self.version def self.version
'2.7.2' '2.7.3'
end end
end end
require 'annotate'
module Annotate module Annotate
module Generators module Generators
class InstallGenerator < Rails::Generators::Base class InstallGenerator < Rails::Generators::Base
desc 'Copy annotate_models rakefiles for automatic annotation' desc 'Copy annotate_models rakefiles for automatic annotation'
source_root File.expand_path('../templates', __FILE__) source_root File.expand_path('templates', __dir__)
# copy rake tasks # copy rake tasks
def copy_tasks def copy_tasks
......
...@@ -42,6 +42,7 @@ if Rails.env.development? ...@@ -42,6 +42,7 @@ if Rails.env.development?
'format_markdown' => 'false', 'format_markdown' => 'false',
'sort' => 'false', 'sort' => 'false',
'force' => 'false', 'force' => 'false',
'classified_sort' => 'true',
'trace' => 'false', 'trace' => 'false',
'wrapper_open' => nil, 'wrapper_open' => nil,
'wrapper_close' => nil, 'wrapper_close' => nil,
......
...@@ -47,6 +47,7 @@ task annotate_models: :environment do ...@@ -47,6 +47,7 @@ task annotate_models: :environment do
options[:ignore_routes] = ENV.fetch('ignore_routes', nil) options[:ignore_routes] = ENV.fetch('ignore_routes', nil)
options[:hide_limit_column_types] = Annotate.fallback(ENV['hide_limit_column_types'], '') options[:hide_limit_column_types] = Annotate.fallback(ENV['hide_limit_column_types'], '')
options[:hide_default_column_types] = Annotate.fallback(ENV['hide_default_column_types'], '') options[:hide_default_column_types] = Annotate.fallback(ENV['hide_default_column_types'], '')
options[:with_comment] = Annotate.fallback(ENV['with_comment'], '')
AnnotateModels.do_annotations(options) AnnotateModels.do_annotations(options)
end end
......
...@@ -8,6 +8,8 @@ task :annotate_routes => :environment do ...@@ -8,6 +8,8 @@ task :annotate_routes => :environment do
options[:position_in_routes] = Annotate.fallback(ENV['position_in_routes'], ENV['position']) options[:position_in_routes] = Annotate.fallback(ENV['position_in_routes'], ENV['position'])
options[:ignore_routes] = Annotate.fallback(ENV['ignore_routes'], nil) options[:ignore_routes] = Annotate.fallback(ENV['ignore_routes'], nil)
options[:require] = ENV['require'] ? ENV['require'].split(',') : [] options[:require] = ENV['require'] ? ENV['require'].split(',') : []
options[:wrapper_open] = Annotate.fallback(ENV['wrapper_open'], ENV['wrapper'])
options[:wrapper_close] = Annotate.fallback(ENV['wrapper_close'], ENV['wrapper'])
AnnotateRoutes.do_annotations(options) AnnotateRoutes.do_annotations(options)
end end
......
...@@ -3,13 +3,17 @@ require File.dirname(__FILE__) + '/../spec_helper.rb' ...@@ -3,13 +3,17 @@ require File.dirname(__FILE__) + '/../spec_helper.rb'
require 'annotate/annotate_models' require 'annotate/annotate_models'
require 'annotate/active_record_patch' require 'annotate/active_record_patch'
require 'active_support/core_ext/string' require 'active_support/core_ext/string'
require 'files'
describe AnnotateModels do describe AnnotateModels do
def mock_index(name, columns = [], unique = false) def mock_index(name, params = {})
double('IndexKeyDefinition', double('IndexKeyDefinition',
name: name, name: name,
columns: columns, columns: params[:columns] || [],
unique: unique) unique: params[:unique] || false,
orders: params[:orders] || {},
where: params[:where],
using: params[:using])
end end
def mock_foreign_key(name, from_column, to_table, to_column = 'id', constraints = {}) def mock_foreign_key(name, from_column, to_table, to_column = 'id', constraints = {})
...@@ -68,6 +72,31 @@ describe AnnotateModels do ...@@ -68,6 +72,31 @@ describe AnnotateModels do
it { expect(AnnotateModels.quote(BigDecimal.new('1.2'))).to eql('1.2') } it { expect(AnnotateModels.quote(BigDecimal.new('1.2'))).to eql('1.2') }
it { expect(AnnotateModels.quote([BigDecimal.new('1.2')])).to eql(['1.2']) } it { expect(AnnotateModels.quote([BigDecimal.new('1.2')])).to eql(['1.2']) }
describe '#parse_options' do
let(:options) do
{
root_dir: '/root',
model_dir: 'app/models,app/one, app/two ,,app/three'
}
end
it 'sets @root_dir' do
AnnotateModels.send(:parse_options, options)
expect(AnnotateModels.instance_variable_get(:@root_dir)).to eq('/root')
end
it 'sets @model_dir separated with a comma' do
AnnotateModels.send(:parse_options, options)
expected = [
'app/models',
'app/one',
'app/two',
'app/three'
]
expect(AnnotateModels.instance_variable_get(:@model_dir)).to eq(expected)
end
end
it 'should get schema info with default options' do it 'should get schema info with default options' do
klass = mock_class(:users, klass = mock_class(:users,
:id, :id,
...@@ -154,7 +183,7 @@ EOS ...@@ -154,7 +183,7 @@ EOS
[ [
mock_column(:id, :integer), mock_column(:id, :integer),
mock_column(:integer, :integer, unsigned?: true), mock_column(:integer, :integer, unsigned?: true),
mock_column(:bigint, :bigint, unsigned?: true), mock_column(:bigint, :integer, unsigned?: true, bigint?: true),
mock_column(:float, :float, unsigned?: true), mock_column(:float, :float, unsigned?: true),
mock_column(:decimal, :decimal, unsigned?: true, precision: 10, scale: 2), mock_column(:decimal, :decimal, unsigned?: true, precision: 10, scale: 2),
]) ])
...@@ -302,8 +331,8 @@ EOS ...@@ -302,8 +331,8 @@ EOS
[ [
mock_column(:id, :integer), mock_column(:id, :integer),
mock_column(:foreign_thing_id, :integer) mock_column(:foreign_thing_id, :integer)
], [mock_index('index_rails_02e851e3b7', ['id']), ], [mock_index('index_rails_02e851e3b7', columns: ['id']),
mock_index('index_rails_02e851e3b8', ['foreign_thing_id'])]) mock_index('index_rails_02e851e3b8', columns: ['foreign_thing_id'])])
expect(AnnotateModels.get_schema_info(klass, 'Schema Info', show_indexes: true)).to eql(<<-EOS) expect(AnnotateModels.get_schema_info(klass, 'Schema Info', show_indexes: true)).to eql(<<-EOS)
# Schema Info # Schema Info
# #
...@@ -320,14 +349,118 @@ EOS ...@@ -320,14 +349,118 @@ EOS
EOS EOS
end end
it 'should get ordered indexes keys' do
klass = mock_class(:users,
:id,
[
mock_column("id", :integer),
mock_column("firstname", :string),
mock_column("surname", :string),
mock_column("value", :string)
],
[
mock_index('index_rails_02e851e3b7', columns: ['id']),
mock_index('index_rails_02e851e3b8',
columns: %w(firstname surname value),
orders: { 'surname' => :asc, 'value' => :desc })
])
expect(AnnotateModels.get_schema_info(klass, 'Schema Info', show_indexes: true)).to eql(<<-EOS)
# Schema Info
#
# Table name: users
#
# id :integer not null, primary key
# firstname :string not null
# surname :string not null
# value :string not null
#
# Indexes
#
# index_rails_02e851e3b7 (id)
# index_rails_02e851e3b8 (firstname,surname ASC,value DESC)
#
EOS
end
it 'should get indexes keys with where clause' do
klass = mock_class(:users,
:id,
[
mock_column("id", :integer),
mock_column("firstname", :string),
mock_column("surname", :string),
mock_column("value", :string)
],
[
mock_index('index_rails_02e851e3b7', columns: ['id']),
mock_index('index_rails_02e851e3b8',
columns: %w(firstname surname),
where: 'value IS NOT NULL')
])
expect(AnnotateModels.get_schema_info(klass, 'Schema Info', show_indexes: true)).to eql(<<-EOS)
# Schema Info
#
# Table name: users
#
# id :integer not null, primary key
# firstname :string not null
# surname :string not null
# value :string not null
#
# Indexes
#
# index_rails_02e851e3b7 (id)
# index_rails_02e851e3b8 (firstname,surname) WHERE value IS NOT NULL
#
EOS
end
it 'should get indexes keys with using clause other than btree' do
klass = mock_class(:users,
:id,
[
mock_column("id", :integer),
mock_column("firstname", :string),
mock_column("surname", :string),
mock_column("value", :string)
],
[
mock_index('index_rails_02e851e3b7', columns: ['id']),
mock_index('index_rails_02e851e3b8',
columns: %w(firstname surname),
using: 'hash')
])
expect(AnnotateModels.get_schema_info(klass, 'Schema Info', show_indexes: true)).to eql(<<-EOS)
# Schema Info
#
# Table name: users
#
# id :integer not null, primary key
# firstname :string not null
# surname :string not null
# value :string not null
#
# Indexes
#
# index_rails_02e851e3b7 (id)
# index_rails_02e851e3b8 (firstname,surname) USING hash
#
EOS
end
it 'should get simple indexes keys' do it 'should get simple indexes keys' do
klass = mock_class(:users, klass = mock_class(:users,
:id, :id,
[ [
mock_column(:id, :integer), mock_column(:id, :integer),
mock_column(:foreign_thing_id, :integer) mock_column(:foreign_thing_id, :integer)
], [mock_index('index_rails_02e851e3b7', ['id']), ],
mock_index('index_rails_02e851e3b8', ['foreign_thing_id'])]) [
mock_index('index_rails_02e851e3b7', columns: ['id']),
mock_index('index_rails_02e851e3b8',
columns: ['foreign_thing_id'],
orders: { 'foreign_thing_id' => :desc })
])
expect(AnnotateModels.get_schema_info(klass, 'Schema Info', simple_indexes: true)).to eql(<<-EOS) expect(AnnotateModels.get_schema_info(klass, 'Schema Info', simple_indexes: true)).to eql(<<-EOS)
# Schema Info # Schema Info
# #
...@@ -345,8 +478,8 @@ EOS ...@@ -345,8 +478,8 @@ EOS
[ [
mock_column("id", :integer), mock_column("id", :integer),
mock_column("name", :string) mock_column("name", :string)
], [mock_index('index_rails_02e851e3b7', ['id']), ], [mock_index('index_rails_02e851e3b7', columns: ['id']),
mock_index('index_rails_02e851e3b8', 'LOWER(name)')]) mock_index('index_rails_02e851e3b8', columns: 'LOWER(name)')])
expect(AnnotateModels.get_schema_info(klass, 'Schema Info', simple_indexes: true)).to eql(<<-EOS) expect(AnnotateModels.get_schema_info(klass, 'Schema Info', simple_indexes: true)).to eql(<<-EOS)
# Schema Info # Schema Info
# #
...@@ -460,8 +593,82 @@ EOS ...@@ -460,8 +593,82 @@ EOS
[ [
mock_column(:id, :integer), mock_column(:id, :integer),
mock_column(:name, :string, limit: 50) mock_column(:name, :string, limit: 50)
], [mock_index('index_rails_02e851e3b7', ['id']), ],
mock_index('index_rails_02e851e3b8', ['foreign_thing_id'])]) [
mock_index('index_rails_02e851e3b7', columns: ['id']),
mock_index('index_rails_02e851e3b8',
columns: ['foreign_thing_id'])
])
expect(AnnotateModels.get_schema_info(klass, AnnotateModels::PREFIX, format_markdown: true, show_indexes: true)).to eql(<<-EOS)
# #{AnnotateModels::PREFIX}
#
# Table name: `users`
#
# ### Columns
#
# Name | Type | Attributes
# ----------- | ------------------ | ---------------------------
# **`id`** | `integer` | `not null, primary key`
# **`name`** | `string(50)` | `not null`
#
# ### Indexes
#
# * `index_rails_02e851e3b7`:
# * **`id`**
# * `index_rails_02e851e3b8`:
# * **`foreign_thing_id`**
#
EOS
end
it 'should get schema info as Markdown with unique indexes' do
klass = mock_class(:users,
:id,
[
mock_column(:id, :integer),
mock_column(:name, :string, limit: 50)
],
[
mock_index('index_rails_02e851e3b7', columns: ['id']),
mock_index('index_rails_02e851e3b8',
columns: ['foreign_thing_id'],
unique: true)
])
expect(AnnotateModels.get_schema_info(klass, AnnotateModels::PREFIX, format_markdown: true, show_indexes: true)).to eql(<<-EOS)
# #{AnnotateModels::PREFIX}
#
# Table name: `users`
#
# ### Columns
#
# Name | Type | Attributes
# ----------- | ------------------ | ---------------------------
# **`id`** | `integer` | `not null, primary key`
# **`name`** | `string(50)` | `not null`
#
# ### Indexes
#
# * `index_rails_02e851e3b7`:
# * **`id`**
# * `index_rails_02e851e3b8` (_unique_):
# * **`foreign_thing_id`**
#
EOS
end
it 'should get schema info as Markdown with ordered indexes' do
klass = mock_class(:users,
:id,
[
mock_column(:id, :integer),
mock_column(:name, :string, limit: 50)
],
[
mock_index('index_rails_02e851e3b7', columns: ['id']),
mock_index('index_rails_02e851e3b8',
columns: ['foreign_thing_id'],
orders: { 'foreign_thing_id' => :desc })
])
expect(AnnotateModels.get_schema_info(klass, AnnotateModels::PREFIX, format_markdown: true, show_indexes: true)).to eql(<<-EOS) expect(AnnotateModels.get_schema_info(klass, AnnotateModels::PREFIX, format_markdown: true, show_indexes: true)).to eql(<<-EOS)
# #{AnnotateModels::PREFIX} # #{AnnotateModels::PREFIX}
# #
...@@ -479,11 +686,97 @@ EOS ...@@ -479,11 +686,97 @@ EOS
# * `index_rails_02e851e3b7`: # * `index_rails_02e851e3b7`:
# * **`id`** # * **`id`**
# * `index_rails_02e851e3b8`: # * `index_rails_02e851e3b8`:
# * **`foreign_thing_id DESC`**
#
EOS
end
it 'should get schema info as Markdown with indexes with WHERE clause' do
klass = mock_class(:users,
:id,
[
mock_column(:id, :integer),
mock_column(:name, :string, limit: 50)
],
[
mock_index('index_rails_02e851e3b7', columns: ['id']),
mock_index('index_rails_02e851e3b8',
columns: ['foreign_thing_id'],
unique: true,
where: 'name IS NOT NULL')
])
expect(AnnotateModels.get_schema_info(klass, AnnotateModels::PREFIX, format_markdown: true, show_indexes: true)).to eql(<<-EOS)
# #{AnnotateModels::PREFIX}
#
# Table name: `users`
#
# ### Columns
#
# Name | Type | Attributes
# ----------- | ------------------ | ---------------------------
# **`id`** | `integer` | `not null, primary key`
# **`name`** | `string(50)` | `not null`
#
# ### Indexes
#
# * `index_rails_02e851e3b7`:
# * **`id`**
# * `index_rails_02e851e3b8` (_unique_ _where_ name IS NOT NULL):
# * **`foreign_thing_id`** # * **`foreign_thing_id`**
# #
EOS EOS
end end
it 'should get schema info as Markdown with indexes with using clause other than btree' do
klass = mock_class(:users,
:id,
[
mock_column(:id, :integer),
mock_column(:name, :string, limit: 50)
],
[
mock_index('index_rails_02e851e3b7', columns: ['id']),
mock_index('index_rails_02e851e3b8',
columns: ['foreign_thing_id'],
using: 'hash')
])
expect(AnnotateModels.get_schema_info(klass, AnnotateModels::PREFIX, format_markdown: true, show_indexes: true)).to eql(<<-EOS)
# #{AnnotateModels::PREFIX}
#
# Table name: `users`
#
# ### Columns
#
# Name | Type | Attributes
# ----------- | ------------------ | ---------------------------
# **`id`** | `integer` | `not null, primary key`
# **`name`** | `string(50)` | `not null`
#
# ### Indexes
#
# * `index_rails_02e851e3b7`:
# * **`id`**
# * `index_rails_02e851e3b8` (_using_ hash):
# * **`foreign_thing_id`**
#
EOS
end
describe '#set_defaults' do
it 'should default show_complete_foreign_keys to false' do
expect(Annotate.true?(ENV['show_complete_foreign_keys'])).to be(false)
end
it 'should be able to set show_complete_foreign_keys to true' do
Annotate.set_defaults('show_complete_foreign_keys' => 'true')
expect(Annotate.true?(ENV['show_complete_foreign_keys'])).to be(true)
end
after :each do
ENV.delete('show_complete_foreign_keys')
end
end
describe '#get_schema_info with custom options' do describe '#get_schema_info with custom options' do
def self.when_called_with(options = {}) def self.when_called_with(options = {})
expected = options.delete(:returns) expected = options.delete(:returns)
...@@ -623,7 +916,8 @@ EOS ...@@ -623,7 +916,8 @@ EOS
[:id, :integer, { limit: 8, comment: 'ID' }], [:id, :integer, { limit: 8, comment: 'ID' }],
[:active, :boolean, { limit: 1, comment: 'Active' }], [:active, :boolean, { limit: 1, comment: 'Active' }],
[:name, :string, { limit: 50, comment: 'Name' }], [:name, :string, { limit: 50, comment: 'Name' }],
[:notes, :text, { limit: 55, comment: 'Notes' }] [:notes, :text, { limit: 55, comment: 'Notes' }],
[:no_comment, :text, { limit: 20, comment: nil }]
] ]
when_called_with with_comment: 'yes', when_called_with with_comment: 'yes',
...@@ -637,6 +931,7 @@ EOS ...@@ -637,6 +931,7 @@ EOS
# active(Active) :boolean not null # active(Active) :boolean not null
# name(Name) :string(50) not null # name(Name) :string(50) not null
# notes(Notes) :text(55) not null # notes(Notes) :text(55) not null
# no_comment :text(20) not null
# #
EOS EOS
...@@ -684,6 +979,121 @@ EOS ...@@ -684,6 +979,121 @@ EOS
end end
end end
describe '#get_model_files' do
subject { described_class.get_model_files(options) }
before do
ARGV.clear
described_class.model_dir = [model_dir]
end
context 'when `model_dir` is valid' do
let(:model_dir) do
Files do
file 'foo.rb'
dir 'bar' do
file 'baz.rb'
dir 'qux' do
file 'quux.rb'
end
end
dir 'concerns' do
file 'corge.rb'
end
end
end
context 'when the model files are not specified' do
context 'when no option is specified' do
let(:options) { {} }
it 'returns all model files under `model_dir` directory' do
is_expected.to contain_exactly(
[model_dir, 'foo.rb'],
[model_dir, File.join('bar', 'baz.rb')],
[model_dir, File.join('bar', 'qux', 'quux.rb')]
)
end
end
context 'when `ignore_model_sub_dir` option is enabled' do
let(:options) { { ignore_model_sub_dir: true } }
it 'returns model files just below `model_dir` directory' do
is_expected.to contain_exactly([model_dir, 'foo.rb'])
end
end
end
context 'when the model files are specified' do
let(:additional_model_dir) { 'additional_model' }
let(:model_files) do
[
File.join(model_dir, 'foo.rb'),
"./#{File.join(additional_model_dir, 'corge/grault.rb')}" # Specification by relative path
]
end
before { ARGV.concat(model_files) }
context 'when no option is specified' do
let(:options) { {} }
context 'when all the specified files are in `model_dir` directory' do
before do
described_class.model_dir << additional_model_dir
end
it 'returns specified files' do
is_expected.to contain_exactly(
[model_dir, 'foo.rb'],
[additional_model_dir, 'corge/grault.rb']
)
end
end
context 'when a model file outside `model_dir` directory is specified' do
it 'exits with the status code' do
begin
subject
raise
rescue SystemExit => e
expect(e.status).to eq(1)
end
end
end
end
context 'when `is_rake` option is enabled' do
let(:options) { { is_rake: true } }
it 'returns all model files under `model_dir` directory' do
is_expected.to contain_exactly(
[model_dir, 'foo.rb'],
[model_dir, File.join('bar', 'baz.rb')],
[model_dir, File.join('bar', 'qux', 'quux.rb')]
)
end
end
end
end
context 'when `model_dir` is invalid' do
let(:model_dir) { '/not_exist_path' }
let(:options) { {} }
it 'exits with the status code' do
begin
subject
raise
rescue SystemExit => e
expect(e.status).to eq(1)
end
end
end
end
describe '#get_model_class' do describe '#get_model_class' do
require 'tmpdir' require 'tmpdir'
...@@ -847,11 +1257,29 @@ EOS ...@@ -847,11 +1257,29 @@ EOS
EOS EOS
path = File.expand_path('loaded_class', AnnotateModels.model_dir[0]) path = File.expand_path('loaded_class', AnnotateModels.model_dir[0])
Kernel.load "#{path}.rb" Kernel.load "#{path}.rb"
expect(Kernel).not_to receive(:require).with(path) expect(Kernel).not_to receive(:require)
expect(capturing(:stderr) do expect(capturing(:stderr) do
check_class_name 'loaded_class.rb', 'LoadedClass' check_class_name 'loaded_class.rb', 'LoadedClass'
end).not_to include('warning: already initialized constant LoadedClass::CONSTANT') end).to be_blank
end
it 'should not require model files twice which is inside a subdirectory' do
dir = Array.new(8) { (0..9).to_a.sample(random: Random.new) }.join
$LOAD_PATH.unshift(File.join(AnnotateModels.model_dir[0], dir))
create "#{dir}/subdir_loaded_class.rb", <<-EOS
class SubdirLoadedClass < ActiveRecord::Base
CONSTANT = 1
end
EOS
path = File.expand_path("#{dir}/subdir_loaded_class", AnnotateModels.model_dir[0])
Kernel.load "#{path}.rb"
expect(Kernel).not_to receive(:require)
expect(capturing(:stderr) do
check_class_name "#{dir}/subdir_loaded_class.rb", 'SubdirLoadedClass'
end).to be_blank
end end
end end
...@@ -898,6 +1326,28 @@ end ...@@ -898,6 +1326,28 @@ end
EOS EOS
end end
it 'should remove annotate if CRLF is used for line breaks' do
path = create 'before.rb', <<-EOS
# == Schema Information
#
# Table name: foo\r\n#
# id :integer not null, primary key
# created_at :datetime
# updated_at :datetime
#
\r\n
class Foo < ActiveRecord::Base
end
EOS
AnnotateModels.remove_annotation_of_file(path)
expect(content(path)).to eq <<-EOS
class Foo < ActiveRecord::Base
end
EOS
end
it 'should remove after annotate' do it 'should remove after annotate' do
path = create 'after.rb', <<-EOS path = create 'after.rb', <<-EOS
class Foo < ActiveRecord::Base class Foo < ActiveRecord::Base
...@@ -946,6 +1396,29 @@ end ...@@ -946,6 +1396,29 @@ end
EOS EOS
end end
it 'should remove wrapper if CRLF is used for line breaks' do
path = create 'opening_wrapper.rb', <<-EOS
# wrapper\r\n# == 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, wrapper_open: 'wrapper')
expect(content(path)).to eq <<-EOS
class Foo < ActiveRecord::Base
end
EOS
end
it 'should remove closing wrapper' do it 'should remove closing wrapper' do
path = create 'closing_wrapper.rb', <<-EOS path = create 'closing_wrapper.rb', <<-EOS
class Foo < ActiveRecord::Base class Foo < ActiveRecord::Base
...@@ -970,6 +1443,28 @@ class Foo < ActiveRecord::Base ...@@ -970,6 +1443,28 @@ class Foo < ActiveRecord::Base
end end
EOS EOS
end end
it 'does not change file with #SkipSchemaAnnotations' do
content = <<-EOS
# -*- SkipSchemaAnnotations
# == Schema Information
#
# Table name: foo
#
# id :integer not null, primary key
# created_at :datetime
# updated_at :datetime
#
class Foo < ActiveRecord::Base
end
EOS
path = create 'skip.rb', content
AnnotateModels.remove_annotation_of_file(path)
expect(content(path)).to eq(content)
end
end end
describe '#resolve_filename' do describe '#resolve_filename' do
...@@ -1164,9 +1659,33 @@ end ...@@ -1164,9 +1659,33 @@ end
end end
end end
it 'adds an empty line between magic comments and annotation (position :before)' do
content = "class User < ActiveRecord::Base\nend\n"
magic_comments_list_each do |magic_comment|
model_file_name, = write_model 'user.rb', "#{magic_comment}\n#{content}"
annotate_one_file position: :before
schema_info = AnnotateModels.get_schema_info(@klass, '== Schema Info')
expect(File.read(model_file_name)).to eq("#{magic_comment}\n\n#{schema_info}\n#{content}")
end
end
it 'adds an empty line between magic comments and model file content (position :after)' do
content = "class User < ActiveRecord::Base\nend\n"
magic_comments_list_each do |magic_comment|
model_file_name, = write_model 'user.rb', "#{magic_comment}\n#{content}"
annotate_one_file position: :after
schema_info = AnnotateModels.get_schema_info(@klass, '== Schema Info')
expect(File.read(model_file_name)).to eq("#{magic_comment}\n\n#{content}\n#{schema_info}")
end
end
describe "if a file can't be annotated" do describe "if a file can't be annotated" do
before do before do
allow(AnnotateModels).to receive(:get_loaded_model).with('user').and_return(nil) allow(AnnotateModels).to receive(:get_loaded_model_by_path).with('user').and_return(nil)
write_model('user.rb', <<-EOS) write_model('user.rb', <<-EOS)
class User < ActiveRecord::Base class User < ActiveRecord::Base
...@@ -1175,28 +1694,28 @@ end ...@@ -1175,28 +1694,28 @@ end
EOS EOS
end end
it 'displays an error message' do it 'displays just the error message with trace disabled (default)' do
expect(capturing(:stdout) do error_output = capturing(:stderr) do
AnnotateModels.do_annotations model_dir: @model_dir, is_rake: true AnnotateModels.do_annotations model_dir: @model_dir, is_rake: true
end).to include("Unable to annotate #{@model_dir}/user.rb: oops")
end end
it 'displays the full stack trace with --trace' do expect(error_output).to include("Unable to annotate #{@model_dir}/user.rb: oops")
expect(capturing(:stdout) do expect(error_output).not_to include('/spec/annotate/annotate_models_spec.rb:')
AnnotateModels.do_annotations model_dir: @model_dir, trace: true, is_rake: true end
end).to include('/spec/annotate/annotate_models_spec.rb:')
it 'displays the error message and stacktrace with trace enabled' do
error_output = capturing(:stderr) do
AnnotateModels.do_annotations model_dir: @model_dir, is_rake: true, trace: true
end end
it 'omits the full stack trace without --trace' do expect(error_output).to include("Unable to annotate #{@model_dir}/user.rb: oops")
expect(capturing(:stdout) do expect(error_output).to include('/spec/annotate/annotate_models_spec.rb:')
AnnotateModels.do_annotations model_dir: @model_dir, trace: false, is_rake: true
end).not_to include('/spec/annotate/annotate_models_spec.rb:')
end end
end end
describe "if a file can't be deannotated" do describe "if a file can't be deannotated" do
before do before do
allow(AnnotateModels).to receive(:get_loaded_model).with('user').and_return(nil) allow(AnnotateModels).to receive(:get_loaded_model_by_path).with('user').and_return(nil)
write_model('user.rb', <<-EOS) write_model('user.rb', <<-EOS)
class User < ActiveRecord::Base class User < ActiveRecord::Base
...@@ -1205,22 +1724,22 @@ end ...@@ -1205,22 +1724,22 @@ end
EOS EOS
end end
it 'displays an error message' do it 'displays just the error message with trace disabled (default)' do
expect(capturing(:stdout) do error_output = capturing(:stderr) do
AnnotateModels.remove_annotations model_dir: @model_dir, is_rake: true AnnotateModels.remove_annotations model_dir: @model_dir, is_rake: true
end).to include("Unable to deannotate #{@model_dir}/user.rb: oops")
end end
it 'displays the full stack trace' do expect(error_output).to include("Unable to deannotate #{@model_dir}/user.rb: oops")
expect(capturing(:stdout) do expect(error_output).not_to include("/user.rb:2:in `<class:User>'")
AnnotateModels.remove_annotations model_dir: @model_dir, trace: true, is_rake: true end
end).to include("/user.rb:2:in `<class:User>'")
it 'displays the error message and stacktrace with trace enabled' do
error_output = capturing(:stderr) do
AnnotateModels.remove_annotations model_dir: @model_dir, is_rake: true, trace: true
end end
it 'omits the full stack trace without --trace' do expect(error_output).to include("Unable to deannotate #{@model_dir}/user.rb: oops")
expect(capturing(:stdout) do expect(error_output).to include("/user.rb:2:in `<class:User>'")
AnnotateModels.remove_annotations model_dir: @model_dir, trace: false, is_rake: true
end).not_to include("/user.rb:2:in `<class:User>'")
end end
end end
end end
......
...@@ -11,6 +11,22 @@ describe AnnotateRoutes do ...@@ -11,6 +11,22 @@ describe AnnotateRoutes do
@mock_file ||= double(File, stubs) @mock_file ||= double(File, stubs)
end end
def magic_comments_list_each
[
'# encoding: UTF-8',
'# coding: UTF-8',
'# -*- coding: UTF-8 -*-',
'#encoding: utf-8',
'# encoding: utf-8',
'# -*- encoding : utf-8 -*-',
"# encoding: utf-8\n# frozen_string_literal: true",
"# frozen_string_literal: true\n# encoding: utf-8",
'# frozen_string_literal: true',
'#frozen_string_literal: false',
'# -*- frozen_string_literal : true -*-'
].each { |magic_comment| yield magic_comment }
end
it 'should check if routes.rb exists' do it 'should check if routes.rb exists' do
expect(File).to receive(:exists?).with(ROUTE_FILE).and_return(false) expect(File).to receive(:exists?).with(ROUTE_FILE).and_return(false)
expect(AnnotateRoutes).to receive(:puts).with("Can't find routes.rb") expect(AnnotateRoutes).to receive(:puts).with("Can't find routes.rb")
...@@ -18,16 +34,24 @@ describe AnnotateRoutes do ...@@ -18,16 +34,24 @@ describe AnnotateRoutes do
end end
describe 'Annotate#example' do describe 'Annotate#example' do
before(:each) do let(:rake_routes_content) do
expect(File).to receive(:exists?).with(ROUTE_FILE).and_return(true) " Prefix Verb URI Pattern Controller#Action
expect(File).to receive(:read).with(ROUTE_FILE).and_return("")
expect(AnnotateRoutes).to receive(:`).with('rake routes').and_return(' Prefix Verb URI Pattern Controller#Action
myaction1 GET /url1(.:format) mycontroller1#action myaction1 GET /url1(.:format) mycontroller1#action
myaction2 POST /url2(.:format) mycontroller2#action myaction2 POST /url2(.:format) mycontroller2#action
myaction3 DELETE|GET /url3(.:format) mycontroller3#action') myaction3 DELETE|GET /url3(.:format) mycontroller3#action\n"
end
expect(AnnotateRoutes).to receive(:puts).with(ANNOTATION_ADDED) before(:each) do
expect(File).to receive(:exists?).with(ROUTE_FILE).and_return(true).at_least(:once)
expect(File).to receive(:read).with(ROUTE_FILE).and_return("").at_least(:once)
expect(AnnotateRoutes).to receive(:puts).with(ANNOTATION_ADDED).at_least(:once)
end
context 'without magic comments' do
before(:each) do
expect(AnnotateRoutes).to receive(:`).with('rake routes').and_return(rake_routes_content)
end end
it 'annotate normal' do it 'annotate normal' do
...@@ -56,12 +80,94 @@ describe AnnotateRoutes do ...@@ -56,12 +80,94 @@ describe AnnotateRoutes do
AnnotateRoutes.do_annotations(format_markdown: true) AnnotateRoutes.do_annotations(format_markdown: true)
end end
it 'wraps annotation if wrapper is specified' do
expect(File).to receive(:open).with(ROUTE_FILE, 'wb').and_yield(mock_file)
expect(@mock_file).to receive(:puts).with("
# START
# == Route Map
#
# Prefix Verb URI Pattern Controller#Action
# myaction1 GET /url1(.:format) mycontroller1#action
# myaction2 POST /url2(.:format) mycontroller2#action
# myaction3 DELETE|GET /url3(.:format) mycontroller3#action
# END\n")
AnnotateRoutes.do_annotations(wrapper_open: 'START', wrapper_close: 'END')
end
end
context 'file with magic comments' do
it 'should not remove magic comments' do
magic_comments_list_each do |magic_comment|
expect(AnnotateRoutes).to receive(:`).with('rake routes')
.and_return("#{magic_comment}\n#{rake_routes_content}")
expect(File).to receive(:open).with(ROUTE_FILE, 'wb').and_yield(mock_file)
expect(@mock_file).to receive(:puts).with("
#{magic_comment}
# == Route Map
#
# Prefix Verb URI Pattern Controller#Action
# myaction1 GET /url1(.:format) mycontroller1#action
# myaction2 POST /url2(.:format) mycontroller2#action
# myaction3 DELETE|GET /url3(.:format) mycontroller3#action\n")
AnnotateRoutes.do_annotations
end
end
it 'annotate markdown' do
magic_comments_list_each do |magic_comment|
expect(AnnotateRoutes).to receive(:`).with('rake routes')
.and_return("#{magic_comment}\n#{rake_routes_content}")
expect(File).to receive(:open).with(ROUTE_FILE, 'wb').and_yield(mock_file)
expect(@mock_file).to receive(:puts).with("
#{magic_comment}
# ## Route Map
#
# Prefix | Verb | URI Pattern | Controller#Action
# --------- | ---------- | --------------- | --------------------
# myaction1 | GET | /url1(.:format) | mycontroller1#action
# myaction2 | POST | /url2(.:format) | mycontroller2#action
# myaction3 | DELETE-GET | /url3(.:format) | mycontroller3#action\n")
AnnotateRoutes.do_annotations(format_markdown: true)
end
end
it 'wraps annotation if wrapper is specified' do
magic_comments_list_each do |magic_comment|
expect(AnnotateRoutes).to receive(:`).with('rake routes')
.and_return("#{magic_comment}\n#{rake_routes_content}")
expect(File).to receive(:open).with(ROUTE_FILE, 'wb').and_yield(mock_file)
expect(@mock_file).to receive(:puts).with("
#{magic_comment}
# START
# == Route Map
#
# Prefix Verb URI Pattern Controller#Action
# myaction1 GET /url1(.:format) mycontroller1#action
# myaction2 POST /url2(.:format) mycontroller2#action
# myaction3 DELETE|GET /url3(.:format) mycontroller3#action
# END\n")
AnnotateRoutes.do_annotations(wrapper_open: 'START', wrapper_close: 'END')
end
end
end
end end
describe 'When adding' do describe 'When adding' do
before(:each) do before(:each) do
expect(File).to receive(:exists?).with(ROUTE_FILE).and_return(true) expect(File).to receive(:exists?).with(ROUTE_FILE)
expect(AnnotateRoutes).to receive(:`).with('rake routes').and_return('') .and_return(true).at_least(:once)
expect(AnnotateRoutes).to receive(:`).with('rake routes')
.and_return('').at_least(:once)
end end
it 'should insert annotations if file does not contain annotations' do it 'should insert annotations if file does not contain annotations' do
...@@ -97,6 +203,42 @@ describe AnnotateRoutes do ...@@ -97,6 +203,42 @@ describe AnnotateRoutes do
AnnotateRoutes.do_annotations AnnotateRoutes.do_annotations
end end
context 'file with magic comments' do
it 'leaves magic comment on top, adds an empty line between magic comment and annotation (position_in_routes :top)' do
expect(File).to receive(:open).with(ROUTE_FILE, 'wb')
.and_yield(mock_file).at_least(:once)
magic_comments_list_each do |magic_comment|
expect(File).to receive(:read).with(ROUTE_FILE).and_return("#{magic_comment}\nSomething")
expect(@mock_file).to receive(:puts).with("#{magic_comment}\n\n# == Route Map\n#\n\nSomething\n")
expect(AnnotateRoutes).to receive(:puts).with(ANNOTATION_ADDED)
AnnotateRoutes.do_annotations(position_in_routes: 'top')
end
end
it 'leaves magic comment on top, adds an empty line between magic comment and annotation (position_in_routes :bottom)' do
expect(File).to receive(:open).with(ROUTE_FILE, 'wb')
.and_yield(mock_file).at_least(:once)
magic_comments_list_each do |magic_comment|
expect(File).to receive(:read).with(ROUTE_FILE).and_return("#{magic_comment}\nSomething")
expect(@mock_file).to receive(:puts).with("#{magic_comment}\nSomething\n\n# == Route Map\n#\n")
expect(AnnotateRoutes).to receive(:puts).with(ANNOTATION_ADDED)
AnnotateRoutes.do_annotations(position_in_routes: 'bottom')
end
end
it 'skips annotations if file does already contain annotation' do
magic_comments_list_each do |magic_comment|
expect(File).to receive(:read).with(ROUTE_FILE)
.and_return("#{magic_comment}\n\n# == Route Map\n#\n")
expect(AnnotateRoutes).to receive(:puts).with(FILE_UNCHANGED)
AnnotateRoutes.do_annotations
end
end
end
end end
describe 'When adding with older Rake versions' do describe 'When adding with older Rake versions' do
...@@ -155,14 +297,82 @@ describe AnnotateRoutes do ...@@ -155,14 +297,82 @@ describe AnnotateRoutes do
end end
it 'should remove trailing annotation and trim trailing newlines, but leave leading newlines alone' do it 'should remove trailing annotation and trim trailing newlines, but leave leading newlines alone' do
expect(File).to receive(:read).with(ROUTE_FILE).and_return("\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\nActionController::Routing...\nfoo\n\n\n\n\n\n\n\n\n\n\n# == Route Map\n#\n# another good line\n# good line\n") expect(File).to receive(:read).with(ROUTE_FILE).and_return(<<-EOS
expect(@mock_file).to receive(:puts).with(/\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\nActionController::Routing...\nfoo\n/)
ActionController::Routing...
foo
# == Route Map
#
# another good line
# good line
EOS
)
expect(@mock_file).to receive(:puts).with(<<-EOS
ActionController::Routing...
foo
EOS
)
AnnotateRoutes.remove_annotations AnnotateRoutes.remove_annotations
end end
it 'should remove prepended annotation and trim leading newlines, but leave trailing newlines alone' do it 'should remove prepended annotation and trim leading newlines, but leave trailing newlines alone' do
expect(File).to receive(:read).with(ROUTE_FILE).and_return("# == Route Map\n#\n# another good line\n# good line\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\nActionController::Routing...\nfoo\n\n\n\n\n\n\n\n\n\n\n") expect(File).to receive(:read).with(ROUTE_FILE).and_return(<<-EOS
expect(@mock_file).to receive(:puts).with(/ActionController::Routing...\nfoo\n\n\n\n\n\n\n\n\n\n\n/) # == Route Map
#
# another good line
# good line
Rails.application.routes.draw do
root 'root#index'
end
EOS
)
expect(@mock_file).to receive(:puts).with(<<-EOS
Rails.application.routes.draw do
root 'root#index'
end
EOS
)
AnnotateRoutes.remove_annotations
end
it 'should not remove custom comments above route map' do
expect(File).to receive(:read).with(ROUTE_FILE).and_return(<<-EOS
# My comment
# == Route Map
#
# another good line
# good line
Rails.application.routes.draw do
root 'root#index'
end
EOS
)
expect(@mock_file).to receive(:puts).with(<<-EOS
# My comment
Rails.application.routes.draw do
root 'root#index'
end
EOS
)
AnnotateRoutes.remove_annotations AnnotateRoutes.remove_annotations
end end
end end
......
...@@ -63,7 +63,7 @@ module Rails ...@@ -63,7 +63,7 @@ module Rails
end end
rescue Gem::LoadError => load_error rescue Gem::LoadError => load_error
if load_error.message =~ /Could not find RubyGem rails/ if load_error.message =~ /Could not find RubyGem rails/
STDERR.puts %(Missing the Rails #{version} gem. Please `gem install -v=#{version} rails`, update your RAILS_GEM_VERSION setting in config/environment.rb for the Rails version you do have installed, or comment out RAILS_GEM_VERSION to use the latest version installed.) $stderr.puts "Missing the Rails #{version} gem. Please `gem install -v=#{version} rails`, update your RAILS_GEM_VERSION setting in config/environment.rb for the Rails version you do have installed, or comment out RAILS_GEM_VERSION to use the latest version installed."
exit 1 exit 1
else else
raise raise
......
class NoNamespace < ActiveRecord::Base
enum foo: [:bar, :baz]
end
class CreateUsers < ActiveRecord::Migration
def change
create_table :no_namespaces do |t|
t.integer :foo
t.timestamps
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