annotate 2.7.4 → 3.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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