annotate 2.7.4 → 3.1.1

Sign up to get free protection for your applications and to get access to all the features.
data/RELEASE.md ADDED
@@ -0,0 +1,19 @@
1
+ ## Prerequisite
2
+
3
+ - Install "git-flow" (`brew install git-flow`)
4
+ - Install "bump" gem (`gem install bump`)
5
+
6
+
7
+ ## Perform a release
8
+
9
+ - `git flow release start <release>`
10
+ - Update the `CHANGELOG.md` file
11
+ - `bump current`
12
+ - `bump patch`
13
+ - `rm -rf dist`
14
+ - `rake spec`
15
+ - `rake gem`
16
+ - `git flow release finish <release>`
17
+
18
+ - `rake gem:publish`
19
+
data/annotate.gemspec CHANGED
@@ -7,42 +7,23 @@ Gem::Specification.new do |s|
7
7
  s.name = 'annotate'
8
8
  s.version = Annotate.version
9
9
 
10
- s.required_ruby_version = '>= 2.2.0'
10
+ s.required_ruby_version = '>= 2.4.0'
11
11
  s.required_rubygems_version = Gem::Requirement.new('>= 0') if s.respond_to? :required_rubygems_version=
12
12
  s.authors = ['Alex Chaffee', 'Cuong Tran', 'Marcos Piccinini', 'Turadg Aleahmad', 'Jon Frisby']
13
13
  s.description = 'Annotates Rails/ActiveRecord Models, routes, fixtures, and others based on the database schema.'
14
14
  s.email = ['alex@stinky.com', 'cuong.tran@gmail.com', 'x@nofxx.com', 'turadg@aleahmad.net', 'jon@cloudability.com']
15
15
  s.executables = ['annotate']
16
- s.extra_rdoc_files = ['README.rdoc', 'CHANGELOG.rdoc', 'TODO.rdoc']
17
- s.files = [
18
- 'AUTHORS.rdoc',
19
- 'CHANGELOG.rdoc',
20
- 'LICENSE.txt',
21
- 'README.rdoc',
22
- 'TODO.rdoc',
23
- 'annotate.gemspec',
24
- 'bin/annotate',
25
- 'lib/annotate.rb',
26
- 'lib/annotate/active_record_patch.rb',
27
- 'lib/annotate/annotate_models.rb',
28
- 'lib/annotate/annotate_routes.rb',
29
- 'lib/annotate/tasks.rb',
30
- 'lib/annotate/version.rb',
31
- 'lib/generators/annotate/USAGE',
32
- 'lib/generators/annotate/install_generator.rb',
33
- 'lib/generators/annotate/templates/auto_annotate_models.rake',
34
- 'lib/tasks/annotate_models.rake',
35
- 'lib/tasks/annotate_routes.rake',
36
- 'lib/tasks/annotate_models_migrate.rake'
37
- ]
16
+ s.extra_rdoc_files = ['README.md', 'CHANGELOG.md']
17
+ s.files = `git ls-files -z LICENSE.txt *.md *.gemspec bin lib`.split("\x0")
38
18
  s.homepage = 'http://github.com/ctran/annotate_models'
39
19
  s.licenses = ['Ruby']
40
20
  s.require_paths = ['lib']
41
- s.rubyforge_project = 'annotate'
42
21
  s.rubygems_version = '2.1.11'
43
22
  s.summary = 'Annotates Rails Models, routes, fixtures, and others based on the database schema.'
44
23
 
45
24
  s.specification_version = 4 if s.respond_to? :specification_version
46
- s.add_runtime_dependency(%q<rake>, ['>= 10.4', '< 13.0'])
47
- s.add_runtime_dependency(%q<activerecord>, ['>= 3.2', '< 6.0'])
25
+ s.add_runtime_dependency(%q<rake>, '>= 10.4', '< 14.0')
26
+ s.add_runtime_dependency(%q<activerecord>, ['>= 3.2', '< 7.0'])
27
+
28
+ s.metadata = { "github_repo" => "ssh://github.com/ctran/annotate_models" }
48
29
  end
data/bin/annotate CHANGED
@@ -14,199 +14,19 @@ end
14
14
  here = File.expand_path(File.dirname __FILE__)
15
15
  $LOAD_PATH << "#{here}/../lib"
16
16
 
17
- require 'optparse'
18
17
  require 'annotate'
19
- Annotate.bootstrap_rake
20
-
21
- has_set_position = {}
22
- target_action = :do_annotations
23
- positions = %w(before top after bottom)
24
-
25
- OptionParser.new do |opts|
26
- opts.banner = 'Usage: annotate [options] [model_file]*'
27
-
28
- opts.on('-d', '--delete', 'Remove annotations from all model files or the routes.rb file') do
29
- target_action = :remove_annotations
30
- end
31
-
32
- opts.on('-p', '--position [before|top|after|bottom]', positions,
33
- 'Place the annotations at the top (before) or the bottom (after) of the model/test/fixture/factory/route/serializer file(s)') do |p|
34
- ENV['position'] = p
35
- %w(position_in_class position_in_factory position_in_fixture position_in_test position_in_routes position_in_serializer).each do |key|
36
- ENV[key] = p unless has_set_position[key]
37
- end
38
- end
39
-
40
- opts.on('--pc', '--position-in-class [before|top|after|bottom]', positions,
41
- 'Place the annotations at the top (before) or the bottom (after) of the model file') do |p|
42
- ENV['position_in_class'] = p
43
- has_set_position['position_in_class'] = true
44
- end
45
-
46
- opts.on('--pf', '--position-in-factory [before|top|after|bottom]', positions,
47
- 'Place the annotations at the top (before) or the bottom (after) of any factory files') do |p|
48
- ENV['position_in_factory'] = p
49
- has_set_position['position_in_factory'] = true
50
- end
51
-
52
- opts.on('--px', '--position-in-fixture [before|top|after|bottom]', positions,
53
- 'Place the annotations at the top (before) or the bottom (after) of any fixture files') do |p|
54
- ENV['position_in_fixture'] = p
55
- has_set_position['position_in_fixture'] = true
56
- end
57
-
58
- opts.on('--pt', '--position-in-test [before|top|after|bottom]', positions,
59
- 'Place the annotations at the top (before) or the bottom (after) of any test files') do |p|
60
- ENV['position_in_test'] = p
61
- has_set_position['position_in_test'] = true
62
- end
63
-
64
- opts.on('--pr', '--position-in-routes [before|top|after|bottom]', positions,
65
- 'Place the annotations at the top (before) or the bottom (after) of the routes.rb file') do |p|
66
- ENV['position_in_routes'] = p
67
- has_set_position['position_in_routes'] = true
68
- end
69
-
70
- opts.on('--ps', '--position-in-serializer [before|top|after|bottom]', positions,
71
- 'Place the annotations at the top (before) or the bottom (after) of the serializer files') do |p|
72
- ENV['position_in_serializer'] = p
73
- has_set_position['position_in_serializer'] = true
74
- end
75
-
76
- opts.on('--w', '--wrapper STR', 'Wrap annotation with the text passed as parameter.',
77
- 'If --w option is used, the same text will be used as opening and closing') do |p|
78
- ENV['wrapper'] = p
79
- end
80
-
81
- opts.on('--wo', '--wrapper-open STR', 'Annotation wrapper opening.') do |p|
82
- ENV['wrapper_open'] = p
83
- end
84
-
85
- opts.on('--wc', '--wrapper-close STR', 'Annotation wrapper closing') do |p|
86
- ENV['wrapper_close'] = p
87
- end
88
-
89
- opts.on('-r', '--routes', "Annotate routes.rb with the output of 'rake routes'") do
90
- ENV['routes'] = 'true'
91
- end
92
-
93
- opts.on('-a', '--active-admin', 'Annotate active_admin models') do
94
- ENV['active_admin'] = 'true'
95
- end
96
-
97
- opts.on('-v', '--version', 'Show the current version of this gem') do
98
- puts "annotate v#{Annotate.version}"; exit
99
- end
100
-
101
- opts.on('-m', '--show-migration', 'Include the migration version number in the annotation') do
102
- ENV['include_version'] = 'yes'
103
- end
104
-
105
- opts.on('-k', '--show-foreign-keys',
106
- "List the table's foreign key constraints in the annotation") do
107
- ENV['show_foreign_keys'] = 'yes'
108
- end
18
+ require 'annotate/parser'
109
19
 
110
- opts.on('--ck',
111
- '--complete-foreign-keys', 'Complete foreign key names in the annotation') do
112
- ENV['show_foreign_keys'] = 'yes'
113
- ENV['show_complete_foreign_keys'] = 'yes'
114
- end
115
-
116
- opts.on('-i', '--show-indexes',
117
- "List the table's database indexes in the annotation") do
118
- ENV['show_indexes'] = 'yes'
119
- end
120
-
121
- opts.on('-s', '--simple-indexes',
122
- "Concat the column's related indexes in the annotation") do
123
- ENV['simple_indexes'] = 'yes'
124
- end
125
-
126
- opts.on('--model-dir dir',
127
- "Annotate model files stored in dir rather than app/models, separate multiple dirs with commas") do |dir|
128
- ENV['model_dir'] = dir
129
- end
130
-
131
- opts.on('--root-dir dir',
132
- "Annotate files stored within root dir projects, separate multiple dirs with commas") do |dir|
133
- ENV['root_dir'] = dir
134
- end
135
-
136
- opts.on('--ignore-model-subdirects',
137
- "Ignore subdirectories of the models directory") do |dir|
138
- ENV['ignore_model_sub_dir'] = 'yes'
139
- end
140
-
141
- opts.on('--sort',
142
- "Sort columns alphabetically, rather than in creation order") do |dir|
143
- ENV['sort'] = 'yes'
144
- end
145
-
146
- opts.on('--classified-sort',
147
- "Sort columns alphabetically, but first goes id, then the rest columns, then the timestamp columns and then the association columns") do |dir|
148
- ENV['classified_sort'] = 'yes'
149
- end
150
-
151
- opts.on('-R', '--require path',
152
- "Additional file to require before loading models, may be used multiple times") do |path|
153
- if !ENV['require'].blank?
154
- ENV['require'] = ENV['require'] + ",#{path}"
155
- else
156
- ENV['require'] = path
157
- end
158
- end
159
-
160
- opts.on('-e', '--exclude [tests,fixtures,factories,serializers]', Array, "Do not annotate fixtures, test files, factories, and/or serializers") do |exclusions|
161
- exclusions ||= %w(tests fixtures factories)
162
- exclusions.each { |exclusion| ENV["exclude_#{exclusion}"] = 'yes' }
163
- end
164
-
165
- opts.on('-f', '--format [bare|rdoc|markdown]', %w(bare rdoc markdown), 'Render Schema Infomation as plain/RDoc/Markdown') do |fmt|
166
- ENV["format_#{fmt}"] = 'yes'
167
- end
168
-
169
- opts.on('--force', 'Force new annotations even if there are no changes.') do |force|
170
- ENV['force'] = 'yes'
171
- end
172
-
173
- opts.on('--timestamp', 'Include timestamp in (routes) annotation') do
174
- ENV['timestamp'] = 'true'
175
- end
176
-
177
- opts.on('--trace', 'If unable to annotate a file, print the full stack trace, not just the exception message.') do |value|
178
- ENV['trace'] = 'yes'
179
- end
180
-
181
- 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|
182
- ENV['ignore_columns'] = regex
183
- end
184
-
185
- opts.on('--ignore-routes REGEX', "don't annotate routes that match a given REGEX (i.e., `annotate -I '(mobile|resque|pghero)'`") do |regex|
186
- ENV['ignore_routes'] = regex
187
- end
188
-
189
- opts.on('--hide-limit-column-types VALUES', "don't show limit for given column types, separated by commas (i.e., `integer,boolean,text`)") do |values|
190
- ENV['hide_limit_column_types'] = "#{values}"
191
- end
192
-
193
- opts.on('--hide-default-column-types VALUES', "don't show default for given column types, separated by commas (i.e., `json,jsonb,hstore`)") do |values|
194
- ENV['hide_default_column_types'] = "#{values}"
195
- end
20
+ Annotate.bootstrap_rake
196
21
 
197
- opts.on('--ignore-unknown-models', "don't display warnings for bad model files") do |values|
198
- ENV['ignore_unknown_models'] = 'true'
199
- end
22
+ options_result = Annotate::Parser.parse(ARGV)
200
23
 
201
- opts.on('--with-comment', "include database comments in model annotations") do |values|
202
- ENV['with_comment'] = 'true'
203
- end
204
- end.parse!
24
+ exit if options_result[:exit]
205
25
 
206
26
  options = Annotate.setup_options(
207
27
  is_rake: ENV['is_rake'] && !ENV['is_rake'].empty?
208
28
  )
209
- Annotate.eager_load(options) if Annotate.include_models?
29
+ Annotate.eager_load(options) if Annotate::Helpers.include_models?
210
30
 
211
- AnnotateModels.send(target_action, options) if Annotate.include_models?
212
- AnnotateRoutes.send(target_action, options) if Annotate.include_routes?
31
+ AnnotateModels.send(options_result[:target_action], options) if Annotate::Helpers.include_models?
32
+ AnnotateRoutes.send(options_result[:target_action], options) if Annotate::Helpers.include_routes?
data/lib/annotate.rb CHANGED
@@ -1,9 +1,9 @@
1
- # rubocop:disable Metrics/ModuleLength
2
-
3
1
  $LOAD_PATH.unshift(File.dirname(__FILE__))
4
2
  require 'annotate/version'
5
3
  require 'annotate/annotate_models'
6
4
  require 'annotate/annotate_routes'
5
+ require 'annotate/constants'
6
+ require 'annotate/helpers'
7
7
 
8
8
  begin
9
9
  # ActiveSupport 3.x...
@@ -16,38 +16,6 @@ rescue StandardError
16
16
  end
17
17
 
18
18
  module Annotate
19
- TRUE_RE = /^(true|t|yes|y|1)$/i
20
-
21
- ##
22
- # The set of available options to customize the behavior of Annotate.
23
- #
24
- POSITION_OPTIONS = [
25
- :position_in_routes, :position_in_class, :position_in_test,
26
- :position_in_fixture, :position_in_factory, :position,
27
- :position_in_serializer
28
- ].freeze
29
- FLAG_OPTIONS = [
30
- :show_indexes, :simple_indexes, :include_version, :exclude_tests,
31
- :exclude_fixtures, :exclude_factories, :ignore_model_sub_dir,
32
- :format_bare, :format_rdoc, :format_markdown, :sort, :force, :trace,
33
- :timestamp, :exclude_serializers, :classified_sort,
34
- :show_foreign_keys, :show_complete_foreign_keys,
35
- :exclude_scaffolds, :exclude_controllers, :exclude_helpers,
36
- :exclude_sti_subclasses, :ignore_unknown_models, :with_comment
37
- ].freeze
38
- OTHER_OPTIONS = [
39
- :ignore_columns, :skip_on_db_migrate, :wrapper_open, :wrapper_close,
40
- :wrapper, :routes, :hide_limit_column_types, :hide_default_column_types,
41
- :ignore_routes, :active_admin
42
- ].freeze
43
- PATH_OPTIONS = [
44
- :require, :model_dir, :root_dir
45
- ].freeze
46
-
47
- def self.all_options
48
- [POSITION_OPTIONS, FLAG_OPTIONS, PATH_OPTIONS, OTHER_OPTIONS]
49
- end
50
-
51
19
  ##
52
20
  # Set default values that can be overridden via environment variables.
53
21
  #
@@ -55,9 +23,9 @@ module Annotate
55
23
  return if @has_set_defaults
56
24
  @has_set_defaults = true
57
25
 
58
- options = HashWithIndifferentAccess.new(options)
26
+ options = ActiveSupport::HashWithIndifferentAccess.new(options)
59
27
 
60
- all_options.flatten.each do |key|
28
+ Constants::ALL_ANNOTATE_OPTIONS.flatten.each do |key|
61
29
  if options.key?(key)
62
30
  default_value = if options[key].is_a?(Array)
63
31
  options[key].join(',')
@@ -75,68 +43,42 @@ module Annotate
75
43
  # TODO: what is the difference between this and set_defaults?
76
44
  #
77
45
  def self.setup_options(options = {})
78
- POSITION_OPTIONS.each do |key|
79
- options[key] = fallback(ENV[key.to_s], ENV['position'], 'before')
46
+ Constants::POSITION_OPTIONS.each do |key|
47
+ options[key] = Annotate::Helpers.fallback(ENV[key.to_s], ENV['position'], 'before')
80
48
  end
81
- FLAG_OPTIONS.each do |key|
82
- options[key] = true?(ENV[key.to_s])
49
+ Constants::FLAG_OPTIONS.each do |key|
50
+ options[key] = Annotate::Helpers.true?(ENV[key.to_s])
83
51
  end
84
- OTHER_OPTIONS.each do |key|
52
+ Constants::OTHER_OPTIONS.each do |key|
85
53
  options[key] = !ENV[key.to_s].blank? ? ENV[key.to_s] : nil
86
54
  end
87
- PATH_OPTIONS.each do |key|
55
+ Constants::PATH_OPTIONS.each do |key|
88
56
  options[key] = !ENV[key.to_s].blank? ? ENV[key.to_s].split(',') : []
89
57
  end
90
58
 
59
+ options[:additional_file_patterns] ||= []
60
+ options[:additional_file_patterns] = options[:additional_file_patterns].split(',') if options[:additional_file_patterns].is_a?(String)
91
61
  options[:model_dir] = ['app/models'] if options[:model_dir].empty?
92
62
 
93
63
  options[:wrapper_open] ||= options[:wrapper]
94
64
  options[:wrapper_close] ||= options[:wrapper]
95
65
 
96
66
  # These were added in 2.7.0 but so this is to revert to old behavior by default
97
- options[:exclude_scaffolds] = Annotate.true?(ENV.fetch('exclude_scaffolds', 'true'))
98
- options[:exclude_controllers] = Annotate.true?(ENV.fetch('exclude_controllers', 'true'))
99
- options[:exclude_helpers] = Annotate.true?(ENV.fetch('exclude_helpers', 'true'))
67
+ options[:exclude_scaffolds] = Annotate::Helpers.true?(ENV.fetch('exclude_scaffolds', 'true'))
68
+ options[:exclude_controllers] = Annotate::Helpers.true?(ENV.fetch('exclude_controllers', 'true'))
69
+ options[:exclude_helpers] = Annotate::Helpers.true?(ENV.fetch('exclude_helpers', 'true'))
100
70
 
101
71
  options
102
72
  end
103
73
 
104
- def self.reset_options
105
- all_options.flatten.each { |key| ENV[key.to_s] = nil }
106
- end
107
-
108
- def self.skip_on_migration?
109
- ENV['ANNOTATE_SKIP_ON_DB_MIGRATE'] =~ TRUE_RE || ENV['skip_on_db_migrate'] =~ TRUE_RE
110
- end
111
-
112
- def self.include_routes?
113
- ENV['routes'] =~ TRUE_RE
114
- end
115
-
116
- def self.include_models?
117
- ENV['routes'] !~ TRUE_RE
118
- end
119
-
120
- def self.loaded_tasks=(val)
121
- @loaded_tasks = val
122
- end
123
-
124
- def self.loaded_tasks
125
- @loaded_tasks
126
- end
127
-
128
74
  def self.load_tasks
129
- return if loaded_tasks
130
- self.loaded_tasks = true
75
+ return if @tasks_loaded
131
76
 
132
77
  Dir[File.join(File.dirname(__FILE__), 'tasks', '**/*.rake')].each do |rake|
133
78
  load rake
134
79
  end
135
- end
136
80
 
137
- def self.load_requires(options)
138
- options[:require].count > 0 &&
139
- options[:require].each { |path| require path }
81
+ @tasks_loaded = true
140
82
  end
141
83
 
142
84
  def self.eager_load(options)
@@ -192,13 +134,12 @@ module Annotate
192
134
  Rake::Task[:set_annotation_options].invoke
193
135
  end
194
136
 
195
- def self.fallback(*args)
196
- args.detect { |arg| !arg.blank? }
197
- end
137
+ class << self
138
+ private
198
139
 
199
- def self.true?(val)
200
- return false if val.blank?
201
- return false unless val =~ TRUE_RE
202
- true
140
+ def load_requires(options)
141
+ options[:require].count > 0 &&
142
+ options[:require].each { |path| require path }
143
+ end
203
144
  end
204
145
  end
@@ -2,9 +2,9 @@
2
2
 
3
3
  require 'bigdecimal'
4
4
 
5
- module AnnotateModels
6
- TRUE_RE = /^(true|t|yes|y|1)$/i
5
+ require 'annotate/constants'
7
6
 
7
+ module AnnotateModels
8
8
  # Annotate Models plugin use this header
9
9
  COMPAT_PREFIX = '== Schema Info'.freeze
10
10
  COMPAT_PREFIX_MD = '## Schema Info'.freeze
@@ -38,9 +38,9 @@ module AnnotateModels
38
38
  BLUEPRINTS_TEST_DIR = File.join('test', "blueprints")
39
39
  BLUEPRINTS_SPEC_DIR = File.join('spec', "blueprints")
40
40
 
41
- # Factory Girl http://github.com/thoughtbot/factory_girl
42
- FACTORY_GIRL_TEST_DIR = File.join('test', "factories")
43
- FACTORY_GIRL_SPEC_DIR = File.join('spec', "factories")
41
+ # Factory Bot https://github.com/thoughtbot/factory_bot
42
+ FACTORY_BOT_TEST_DIR = File.join('test', "factories")
43
+ FACTORY_BOT_SPEC_DIR = File.join('spec', "factories")
44
44
 
45
45
  # Fabrication https://github.com/paulelliott/fabrication.git
46
46
  FABRICATORS_TEST_DIR = File.join('test', "fabricators")
@@ -62,7 +62,7 @@ module AnnotateModels
62
62
 
63
63
  # Don't show limit (#) on these column types
64
64
  # Example: show "integer" instead of "integer(4)"
65
- NO_LIMIT_COL_TYPES = %w(integer boolean).freeze
65
+ NO_LIMIT_COL_TYPES = %w(integer bigint boolean).freeze
66
66
 
67
67
  # Don't show default value for these column types
68
68
  NO_DEFAULT_COL_TYPES = %w(json jsonb hstore).freeze
@@ -82,6 +82,8 @@ module AnnotateModels
82
82
  }
83
83
  }.freeze
84
84
 
85
+ 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))/).freeze
86
+
85
87
  class << self
86
88
  def annotate_pattern(options = {})
87
89
  if options[:wrapper_open]
@@ -140,12 +142,12 @@ module AnnotateModels
140
142
  File.join(root_directory, EXEMPLARS_SPEC_DIR, "%MODEL_NAME%_exemplar.rb"),
141
143
  File.join(root_directory, BLUEPRINTS_TEST_DIR, "%MODEL_NAME%_blueprint.rb"),
142
144
  File.join(root_directory, BLUEPRINTS_SPEC_DIR, "%MODEL_NAME%_blueprint.rb"),
143
- File.join(root_directory, FACTORY_GIRL_TEST_DIR, "%MODEL_NAME%_factory.rb"), # (old style)
144
- File.join(root_directory, FACTORY_GIRL_SPEC_DIR, "%MODEL_NAME%_factory.rb"), # (old style)
145
- File.join(root_directory, FACTORY_GIRL_TEST_DIR, "%TABLE_NAME%.rb"), # (new style)
146
- File.join(root_directory, FACTORY_GIRL_SPEC_DIR, "%TABLE_NAME%.rb"), # (new style)
147
- File.join(root_directory, FACTORY_GIRL_TEST_DIR, "%PLURALIZED_MODEL_NAME%.rb"), # (new style)
148
- File.join(root_directory, FACTORY_GIRL_SPEC_DIR, "%PLURALIZED_MODEL_NAME%.rb"), # (new style)
145
+ File.join(root_directory, FACTORY_BOT_TEST_DIR, "%MODEL_NAME%_factory.rb"), # (old style)
146
+ File.join(root_directory, FACTORY_BOT_SPEC_DIR, "%MODEL_NAME%_factory.rb"), # (old style)
147
+ File.join(root_directory, FACTORY_BOT_TEST_DIR, "%TABLE_NAME%.rb"), # (new style)
148
+ File.join(root_directory, FACTORY_BOT_SPEC_DIR, "%TABLE_NAME%.rb"), # (new style)
149
+ File.join(root_directory, FACTORY_BOT_TEST_DIR, "%PLURALIZED_MODEL_NAME%.rb"), # (new style)
150
+ File.join(root_directory, FACTORY_BOT_SPEC_DIR, "%PLURALIZED_MODEL_NAME%.rb"), # (new style)
149
151
  File.join(root_directory, FABRICATORS_TEST_DIR, "%MODEL_NAME%_fabricator.rb"),
150
152
  File.join(root_directory, FABRICATORS_SPEC_DIR, "%MODEL_NAME%_fabricator.rb")
151
153
  ]
@@ -154,18 +156,20 @@ module AnnotateModels
154
156
  def serialize_files(root_directory)
155
157
  [
156
158
  File.join(root_directory, SERIALIZERS_DIR, "%MODEL_NAME%_serializer.rb"),
157
- File.join(root_directory, SERIALIZERS_TEST_DIR, "%MODEL_NAME%_serializer_spec.rb"),
159
+ File.join(root_directory, SERIALIZERS_TEST_DIR, "%MODEL_NAME%_serializer_test.rb"),
158
160
  File.join(root_directory, SERIALIZERS_SPEC_DIR, "%MODEL_NAME%_serializer_spec.rb")
159
161
  ]
160
162
  end
161
163
 
162
- def files_by_pattern(root_directory, pattern_type)
164
+ def files_by_pattern(root_directory, pattern_type, options)
163
165
  case pattern_type
164
166
  when 'test' then test_files(root_directory)
165
167
  when 'fixture' then fixture_files(root_directory)
166
168
  when 'scaffold' then scaffold_files(root_directory)
167
169
  when 'factory' then factory_files(root_directory)
168
170
  when 'serializer' then serialize_files(root_directory)
171
+ when 'additional_file_patterns'
172
+ [options[:additional_file_patterns] || []].flatten
169
173
  when 'controller'
170
174
  [File.join(root_directory, CONTROLLER_DIR, "%PLURALIZED_MODEL_NAME%_controller.rb")]
171
175
  when 'admin'
@@ -177,14 +181,20 @@ module AnnotateModels
177
181
  end
178
182
  end
179
183
 
180
- def get_patterns(pattern_types = [])
184
+ def get_patterns(options, pattern_types = [])
181
185
  current_patterns = []
182
186
  root_dir.each do |root_directory|
183
187
  Array(pattern_types).each do |pattern_type|
184
- current_patterns += files_by_pattern(root_directory, pattern_type)
188
+ patterns = files_by_pattern(root_directory, pattern_type, options)
189
+
190
+ current_patterns += if pattern_type.to_sym == :additional_file_patterns
191
+ patterns
192
+ else
193
+ patterns.map { |p| p.sub(/^[\/]*/, '') }
194
+ end
185
195
  end
186
196
  end
187
- current_patterns.map { |p| p.sub(/^[\/]*/, '') }
197
+ current_patterns
188
198
  end
189
199
 
190
200
  # Simple quoting for the default column value
@@ -236,16 +246,7 @@ module AnnotateModels
236
246
  info << "# #{ '-' * ( max_size + md_names_overhead ) } | #{'-' * md_type_allowance} | #{ '-' * 27 }\n"
237
247
  end
238
248
 
239
- cols = if ignore_columns = options[:ignore_columns]
240
- klass.columns.reject do |col|
241
- col.name.match(/#{ignore_columns}/)
242
- end
243
- else
244
- klass.columns
245
- end
246
-
247
- cols = cols.sort_by(&:name) if options[:sort]
248
- cols = classified_sort(cols) if options[:classified_sort]
249
+ cols = columns(klass, options)
249
250
  cols.each do |col|
250
251
  col_type = get_col_type(col)
251
252
  attrs = []
@@ -256,8 +257,8 @@ module AnnotateModels
256
257
 
257
258
  if col_type == 'decimal'
258
259
  col_type << "(#{col.precision}, #{col.scale})"
259
- elsif col_type != 'spatial'
260
- if col.limit
260
+ elsif !%w[spatial geometry geography].include?(col_type)
261
+ if col.limit && !options[:format_yard]
261
262
  if col.limit.is_a? Array
262
263
  attrs << "(#{col.limit.join(', ')})"
263
264
  else
@@ -296,12 +297,16 @@ module AnnotateModels
296
297
  end
297
298
  if options[:format_rdoc]
298
299
  info << sprintf("# %-#{max_size}.#{max_size}s<tt>%s</tt>", "*#{col_name}*::", attrs.unshift(col_type).join(", ")).rstrip + "\n"
300
+ elsif options[:format_yard]
301
+ info << sprintf("# @!attribute #{col_name}") + "\n"
302
+ ruby_class = col.respond_to?(:array) && col.array ? "Array<#{map_col_type_to_ruby_classes(col_type)}>": map_col_type_to_ruby_classes(col_type)
303
+ info << sprintf("# @return [#{ruby_class}]") + "\n"
299
304
  elsif options[:format_markdown]
300
- name_remainder = max_size - col_name.length
305
+ name_remainder = max_size - col_name.length - non_ascii_length(col_name)
301
306
  type_remainder = (md_type_allowance - 2) - col_type.length
302
307
  info << (sprintf("# **`%s`**%#{name_remainder}s | `%s`%#{type_remainder}s | `%s`", col_name, " ", col_type, " ", attrs.join(", ").rstrip)).gsub('``', ' ').rstrip + "\n"
303
308
  else
304
- info << sprintf("# %-#{max_size}.#{max_size}s:%-#{bare_type_allowance}.#{bare_type_allowance}s %s", col_name, col_type, attrs.join(", ")).rstrip + "\n"
309
+ info << format_default(col_name, max_size, col_type, bare_type_allowance, attrs)
305
310
  end
306
311
  end
307
312
 
@@ -362,7 +367,7 @@ module AnnotateModels
362
367
  end
363
368
 
364
369
  def get_col_type(col)
365
- if col.respond_to?(:bigint?) && col.bigint?
370
+ if (col.respond_to?(:bigint?) && col.bigint?) || /\Abigint\b/ =~ col.sql_type
366
371
  'bigint'
367
372
  else
368
373
  (col.type || col.sql_type).to_s
@@ -464,7 +469,10 @@ module AnnotateModels
464
469
  foreign_keys = klass.connection.foreign_keys(klass.table_name)
465
470
  return '' if foreign_keys.empty?
466
471
 
467
- format_name = ->(fk) { options[:show_complete_foreign_keys] ? fk.name : fk.name.gsub(/(?<=^fk_rails_)[0-9a-f]{10}$/, '...') }
472
+ format_name = lambda do |fk|
473
+ return fk.options[:column] if fk.name.blank?
474
+ options[:show_complete_foreign_keys] ? fk.name : fk.name.gsub(/(?<=^fk_rails_)[0-9a-f]{10}$/, '...')
475
+ end
468
476
 
469
477
  max_size = foreign_keys.map(&format_name).map(&:size).max + 1
470
478
  foreign_keys.sort_by {|fk| [format_name.call(fk), fk.column]}.each do |fk|
@@ -498,64 +506,63 @@ module AnnotateModels
498
506
  # :before, :top, :after or :bottom. Default is :before.
499
507
  #
500
508
  def annotate_one_file(file_name, info_block, position, options = {})
501
- if File.exist?(file_name)
502
- old_content = File.read(file_name)
503
- return false if old_content =~ /#{SKIP_ANNOTATION_PREFIX}.*\n/
509
+ return false unless File.exist?(file_name)
510
+ old_content = File.read(file_name)
511
+ return false if old_content =~ /#{SKIP_ANNOTATION_PREFIX}.*\n/
504
512
 
505
- # Ignore the Schema version line because it changes with each migration
506
- header_pattern = /(^# Table name:.*?\n(#.*[\r]?\n)*[\r]?)/
507
- old_header = old_content.match(header_pattern).to_s
508
- new_header = info_block.match(header_pattern).to_s
513
+ # Ignore the Schema version line because it changes with each migration
514
+ header_pattern = /(^# Table name:.*?\n(#.*[\r]?\n)*[\r]?)/
515
+ old_header = old_content.match(header_pattern).to_s
516
+ new_header = info_block.match(header_pattern).to_s
509
517
 
510
- column_pattern = /^#[\t ]+[\w\*`]+[\t ]+.+$/
511
- old_columns = old_header && old_header.scan(column_pattern).sort
512
- new_columns = new_header && new_header.scan(column_pattern).sort
518
+ column_pattern = /^#[\t ]+[\w\*\.`]+[\t ]+.+$/
519
+ old_columns = old_header && old_header.scan(column_pattern).sort
520
+ new_columns = new_header && new_header.scan(column_pattern).sort
513
521
 
514
- magic_comments_block = magic_comments_as_string(old_content)
522
+ return false if old_columns == new_columns && !options[:force]
515
523
 
516
- if old_columns == new_columns && !options[:force]
517
- return false
518
- else
519
- # Replace inline the old schema info with the new schema info
520
- new_content = old_content.sub(annotate_pattern(options), info_block + "\n")
524
+ abort "annotate error. #{file_name} needs to be updated, but annotate was run with `--frozen`." if options[:frozen]
521
525
 
522
- if new_content.end_with?(info_block + "\n")
523
- new_content = old_content.sub(annotate_pattern(options), "\n" + info_block)
524
- end
526
+ # Replace inline the old schema info with the new schema info
527
+ wrapper_open = options[:wrapper_open] ? "# #{options[:wrapper_open]}\n" : ""
528
+ wrapper_close = options[:wrapper_close] ? "# #{options[:wrapper_close]}\n" : ""
529
+ wrapped_info_block = "#{wrapper_open}#{info_block}#{wrapper_close}"
525
530
 
526
- wrapper_open = options[:wrapper_open] ? "# #{options[:wrapper_open]}\n" : ""
527
- wrapper_close = options[:wrapper_close] ? "# #{options[:wrapper_close]}\n" : ""
528
- wrapped_info_block = "#{wrapper_open}#{info_block}#{wrapper_close}"
529
- # if there *was* no old schema info (no substitution happened) or :force was passed,
530
- # we simply need to insert it in correct position
531
- if new_content == old_content || options[:force]
532
- old_content.gsub!(magic_comment_matcher, '')
533
- old_content.sub!(annotate_pattern(options), '')
534
-
535
- new_content = if %w(after bottom).include?(options[position].to_s)
536
- magic_comments_block + (old_content.rstrip + "\n\n" + wrapped_info_block)
537
- else
538
- magic_comments_block + wrapped_info_block + "\n" + old_content
539
- end
540
- end
531
+ old_annotation = old_content.match(annotate_pattern(options)).to_s
541
532
 
542
- File.open(file_name, 'wb') { |f| f.puts new_content }
543
- return true
544
- end
533
+ # if there *was* no old schema info or :force was passed, we simply
534
+ # need to insert it in correct position
535
+ if old_annotation.empty? || options[:force]
536
+ magic_comments_block = magic_comments_as_string(old_content)
537
+ old_content.gsub!(MAGIC_COMMENT_MATCHER, '')
538
+ old_content.sub!(annotate_pattern(options), '')
539
+
540
+ new_content = if %w(after bottom).include?(options[position].to_s)
541
+ magic_comments_block + (old_content.rstrip + "\n\n" + wrapped_info_block)
542
+ elsif magic_comments_block.empty?
543
+ magic_comments_block + wrapped_info_block + old_content.lstrip
544
+ else
545
+ magic_comments_block + "\n" + wrapped_info_block + old_content.lstrip
546
+ end
545
547
  else
546
- false
548
+ # replace the old annotation with the new one
549
+
550
+ # keep the surrounding whitespace the same
551
+ space_match = old_annotation.match(/\A(?<start>\s*).*?\n(?<end>\s*)\z/m)
552
+ new_annotation = space_match[:start] + wrapped_info_block + space_match[:end]
553
+
554
+ new_content = old_content.sub(annotate_pattern(options), new_annotation)
547
555
  end
548
- end
549
556
 
550
- def magic_comment_matcher
551
- 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))/)
557
+ File.open(file_name, 'wb') { |f| f.puts new_content }
558
+ true
552
559
  end
553
560
 
554
561
  def magic_comments_as_string(content)
555
- magic_comments = content.scan(magic_comment_matcher).flatten.compact
562
+ magic_comments = content.scan(MAGIC_COMMENT_MATCHER).flatten.compact
556
563
 
557
564
  if magic_comments.any?
558
- magic_comments.join + "\n"
565
+ magic_comments.join
559
566
  else
560
567
  ''
561
568
  end
@@ -578,8 +585,9 @@ module AnnotateModels
578
585
  end
579
586
 
580
587
  def matched_types(options)
581
- types = MATCHED_TYPES
582
- types << 'admin' if options[:active_admin] =~ TRUE_RE && !types.include?('admin')
588
+ types = MATCHED_TYPES.dup
589
+ types << 'admin' if options[:active_admin] =~ Annotate::Constants::TRUE_RE && !types.include?('admin')
590
+ types << 'additional_file_patterns' if options[:additional_file_patterns].present?
583
591
 
584
592
  types
585
593
  end
@@ -631,8 +639,11 @@ module AnnotateModels
631
639
  end
632
640
 
633
641
  next if options[exclusion_key]
634
- get_patterns(key)
642
+
643
+ get_patterns(options, key)
635
644
  .map { |f| resolve_filename(f, model_name, table_name) }
645
+ .map { |f| expand_glob_into_files(f) }
646
+ .flatten
636
647
  .each do |f|
637
648
  if annotate_one_file(f, info, position_key, options_with_position(options, position_key))
638
649
  annotated << f
@@ -790,6 +801,10 @@ module AnnotateModels
790
801
  end
791
802
  end
792
803
 
804
+ def expand_glob_into_files(glob)
805
+ Dir.glob(glob)
806
+ end
807
+
793
808
  def annotate_model_file(annotated, file, header, options)
794
809
  begin
795
810
  return false if /#{SKIP_ANNOTATION_PREFIX}.*/ =~ (File.exist?(file) ? File.read(file) : '')
@@ -827,7 +842,7 @@ module AnnotateModels
827
842
  model_file_name = file
828
843
  deannotated_klass = true if remove_annotation_of_file(model_file_name, options)
829
844
 
830
- get_patterns(matched_types(options))
845
+ get_patterns(options, matched_types(options))
831
846
  .map { |f| resolve_filename(f, model_name, table_name) }
832
847
  .each do |f|
833
848
  if File.exist?(f)
@@ -883,18 +898,104 @@ module AnnotateModels
883
898
  end
884
899
 
885
900
  def max_schema_info_width(klass, options)
901
+ cols = columns(klass, options)
902
+
886
903
  if with_comments?(klass, options)
887
- max_size = klass.columns.map do |column|
888
- column.name.size + (column.comment ? column.comment.size : 0)
904
+ max_size = cols.map do |column|
905
+ column.name.size + (column.comment ? width(column.comment) : 0)
889
906
  end.max || 0
890
907
  max_size += 2
891
908
  else
892
- max_size = klass.column_names.map(&:size).max
909
+ max_size = cols.map(&:name).map(&:size).max
893
910
  end
894
911
  max_size += options[:format_rdoc] ? 5 : 1
895
912
 
896
913
  max_size
897
914
  end
915
+
916
+ def format_default(col_name, max_size, col_type, bare_type_allowance, attrs)
917
+ sprintf("# %s:%s %s", mb_chars_ljust(col_name, max_size), mb_chars_ljust(col_type, bare_type_allowance), attrs.join(", ")).rstrip + "\n"
918
+ end
919
+
920
+ def width(string)
921
+ string.chars.inject(0) { |acc, elem| acc + (elem.bytesize == 3 ? 2 : 1) }
922
+ end
923
+
924
+ def mb_chars_ljust(string, length)
925
+ string = string.to_s
926
+ padding = length - width(string)
927
+ if padding > 0
928
+ string + (' ' * padding)
929
+ else
930
+ string[0..length-1]
931
+ end
932
+ end
933
+
934
+ def non_ascii_length(string)
935
+ string.to_s.chars.reject(&:ascii_only?).length
936
+ end
937
+
938
+ def map_col_type_to_ruby_classes(col_type)
939
+ case col_type
940
+ when 'integer' then Integer.to_s
941
+ when 'float' then Float.to_s
942
+ when 'decimal' then BigDecimal.to_s
943
+ when 'datetime', 'timestamp', 'time' then Time.to_s
944
+ when 'date' then Date.to_s
945
+ when 'text', 'string', 'binary', 'inet', 'uuid' then String.to_s
946
+ when 'json', 'jsonb' then Hash.to_s
947
+ when 'boolean' then 'Boolean'
948
+ end
949
+ end
950
+
951
+ def columns(klass, options)
952
+ cols = klass.columns
953
+ cols += translated_columns(klass)
954
+
955
+ if ignore_columns = options[:ignore_columns]
956
+ cols = cols.reject do |col|
957
+ col.name.match(/#{ignore_columns}/)
958
+ end
959
+ end
960
+
961
+ cols = cols.sort_by(&:name) if options[:sort]
962
+ cols = classified_sort(cols) if options[:classified_sort]
963
+
964
+ cols
965
+ end
966
+
967
+ ##
968
+ # Add columns managed by the globalize gem if this gem is being used.
969
+ def translated_columns(klass)
970
+ return [] unless klass.respond_to? :translation_class
971
+
972
+ ignored_cols = ignored_translation_table_colums(klass)
973
+ klass.translation_class.columns.reject do |col|
974
+ ignored_cols.include? col.name.to_sym
975
+ end
976
+ end
977
+
978
+ ##
979
+ # These are the columns that the globalize gem needs to work but
980
+ # are not necessary for the models to be displayed as annotations.
981
+ def ignored_translation_table_colums(klass)
982
+ # Construct the foreign column name in the translations table
983
+ # eg. Model: Car, foreign column name: car_id
984
+ foreign_column_name = [
985
+ klass.translation_class.to_s
986
+ .gsub('::Translation', '').gsub('::', '_')
987
+ .downcase,
988
+ '_id'
989
+ ].join.to_sym
990
+
991
+ [
992
+ :id,
993
+ :created_at,
994
+ :updated_at,
995
+ :locale,
996
+ foreign_column_name
997
+ ]
998
+ end
898
999
  end
899
1000
 
900
1001
  class BadModelFileError < LoadError