annotate 2.7.2 → 2.7.3
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +5 -5
- data/CHANGELOG.rdoc +3 -0
- data/README.rdoc +24 -8
- data/annotate.gemspec +1 -1
- data/bin/annotate +12 -2
- data/lib/annotate.rb +6 -5
- data/lib/annotate/annotate_models.rb +213 -63
- data/lib/annotate/annotate_routes.rb +65 -14
- data/lib/annotate/version.rb +1 -1
- data/lib/generators/annotate/install_generator.rb +3 -1
- data/lib/generators/annotate/templates/auto_annotate_models.rake +1 -0
- data/lib/tasks/annotate_models.rake +1 -0
- data/lib/tasks/annotate_routes.rake +2 -0
- metadata +4 -4
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
|
-
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: d4cd9e83172aed3fe8975a79f1cfeb076a1af786e547472c4d22130e07c559c9
|
4
|
+
data.tar.gz: a276b3e5964dd0b6fd5c033be8d1d143989abbbe3c43e0a56e399455c644cbe2
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: f44658cc7947064044bbc96fc2d9a4b2e077cc4c6b2934b71477081a0a9e42ad0d01bc8ac0e43d6707f8576cdcbf49af85562441dc7dde5f7517219ffb34902c
|
7
|
+
data.tar.gz: 5d16e2603ef7c5a3428d157f9d85a51a02d6386eeafd984d16a133dd55c1dec61dc14e15be6cf3ae349d34403b358785cba37f0b262b3cccdb9f750fd8043eb6
|
data/CHANGELOG.rdoc
CHANGED
data/README.rdoc
CHANGED
@@ -17,7 +17,7 @@ your...
|
|
17
17
|
- Object Daddy exemplars
|
18
18
|
- Machinist blueprints
|
19
19
|
- Fabrication fabricators
|
20
|
-
- Thoughtbot's
|
20
|
+
- Thoughtbot's factory_bot factories, i.e. the (spec|test)/factories/<model>_factory.rb files
|
21
21
|
- routes.rb file (for Rails projects)
|
22
22
|
|
23
23
|
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
55
|
|
56
56
|
Into Gemfile from rubygems.org:
|
57
57
|
|
58
|
-
|
58
|
+
group :development do
|
59
|
+
gem 'annotate'
|
60
|
+
end
|
59
61
|
|
60
62
|
Into Gemfile from Github:
|
61
63
|
|
62
|
-
|
64
|
+
group :development do
|
65
|
+
gem 'annotate', git: 'https://github.com/ctran/annotate_models.git'
|
66
|
+
end
|
63
67
|
|
64
68
|
Into environment gems from rubygems.org:
|
65
69
|
|
@@ -154,14 +158,15 @@ If you want to run <code>rake db:migrate</code> as a one-off without running ann
|
|
154
158
|
you can do so with a simple environment variable, instead of editing the
|
155
159
|
+.rake+ file:
|
156
160
|
|
157
|
-
|
161
|
+
ANNOTATE_SKIP_ON_DB_MIGRATE=1 rake db:migrate
|
158
162
|
|
159
163
|
|
160
164
|
== Options
|
161
165
|
|
162
166
|
Usage: annotate [options] [model_file]*
|
163
167
|
-d, --delete Remove annotations from all model files or the routes.rb file
|
164
|
-
-p
|
168
|
+
-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)
|
169
|
+
--position
|
165
170
|
--pc, --position-in-class [before|top|after|bottom]
|
166
171
|
Place the annotations at the top (before) or the bottom (after) of the model file
|
167
172
|
--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
184
|
--wo, --wrapper-open STR Annotation wrapper opening.
|
180
185
|
--wc, --wrapper-close STR Annotation wrapper closing
|
181
186
|
-r, --routes Annotate routes.rb with the output of 'rake routes'
|
182
|
-
-
|
187
|
+
-a, --active-admin Annotate active_admin models
|
183
188
|
-v, --version Show the current version of this gem
|
184
189
|
-m, --show-migration Include the migration version number in the annotation
|
185
|
-
-i, --show-indexes List the table's database indexes in the annotation
|
186
190
|
-k, --show-foreign-keys List the table's foreign key constraints in the annotation
|
191
|
+
--ck, --complete-foreign-keys
|
192
|
+
Complete foreign key names in the annotation
|
193
|
+
-i, --show-indexes List the table's database indexes in the annotation
|
187
194
|
-s, --simple-indexes Concat the column's related indexes in the annotation
|
188
195
|
--model-dir dir Annotate model files stored in dir rather than app/models, separate multiple dirs with commas
|
196
|
+
--root-dir dir Annotate files stored within root dir projects, separate multiple dirs with commas
|
189
197
|
--ignore-model-subdirects Ignore subdirectories of the models directory
|
190
198
|
--sort Sort columns alphabetically, rather than in creation order
|
199
|
+
--classified-sort Sort columns alphabetically, but first goes id, then the rest columns, then the timestamp columns and then the association columns
|
191
200
|
-R, --require path Additional file to require before loading models, may be used multiple times
|
192
201
|
-e [tests,fixtures,factories,serializers],
|
193
202
|
--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
206
|
--timestamp Include timestamp in (routes) annotation
|
198
207
|
--trace If unable to annotate a file, print the full stack trace, not just the exception message.
|
199
208
|
-I, --ignore-columns REGEX don't annotate columns that match a given REGEX (i.e., `annotate -I '^(id|updated_at|created_at)'`
|
209
|
+
--ignore-routes REGEX don't annotate routes that match a given REGEX (i.e., `annotate -I '(mobile|resque|pghero)'`
|
210
|
+
--hide-limit-column-types VALUES
|
211
|
+
don't show limit for given column types, separated by commas (i.e., `integer,boolean,text`)
|
212
|
+
--hide-default-column-types VALUES
|
213
|
+
don't show default for given column types, separated by commas (i.e., `json,jsonb,hstore`)
|
214
|
+
--ignore-unknown-models don't display warnings for bad model files
|
215
|
+
--with-comment include database comments in model annotations
|
200
216
|
|
201
217
|
|
202
218
|
|
@@ -242,7 +258,7 @@ extra carefully, and consider using one.
|
|
242
258
|
|
243
259
|
== Links
|
244
260
|
|
245
|
-
- Factory
|
261
|
+
- Factory Bot: http://github.com/thoughtbot/factory_bot
|
246
262
|
- Object Daddy: http://github.com/flogic/object_daddy
|
247
263
|
- Machinist: http://github.com/notahat/machinist
|
248
264
|
- Fabrication: http://github.com/paulelliott/fabrication
|
data/annotate.gemspec
CHANGED
@@ -7,7 +7,7 @@ Gem::Specification.new do |s|
|
|
7
7
|
s.name = 'annotate'
|
8
8
|
s.version = Annotate.version
|
9
9
|
|
10
|
-
s.required_ruby_version = '>=
|
10
|
+
s.required_ruby_version = '>= 2.2.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.'
|
data/bin/annotate
CHANGED
@@ -90,7 +90,7 @@ OptionParser.new do |opts|
|
|
90
90
|
ENV['routes'] = 'true'
|
91
91
|
end
|
92
92
|
|
93
|
-
opts.on('-
|
93
|
+
opts.on('-a', '--active-admin', 'Annotate active_admin models') do
|
94
94
|
ENV['active_admin'] = 'true'
|
95
95
|
end
|
96
96
|
|
@@ -107,6 +107,12 @@ OptionParser.new do |opts|
|
|
107
107
|
ENV['show_foreign_keys'] = 'yes'
|
108
108
|
end
|
109
109
|
|
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
|
+
|
110
116
|
opts.on('-i', '--show-indexes',
|
111
117
|
"List the table's database indexes in the annotation") do
|
112
118
|
ENV['show_indexes'] = 'yes'
|
@@ -191,12 +197,16 @@ OptionParser.new do |opts|
|
|
191
197
|
opts.on('--ignore-unknown-models', "don't display warnings for bad model files") do |values|
|
192
198
|
ENV['ignore_unknown_models'] = 'true'
|
193
199
|
end
|
200
|
+
|
201
|
+
opts.on('--with-comment', "include database comments in model annotations") do |values|
|
202
|
+
ENV['with_comment'] = 'true'
|
203
|
+
end
|
194
204
|
end.parse!
|
195
205
|
|
196
206
|
options = Annotate.setup_options(
|
197
207
|
is_rake: ENV['is_rake'] && !ENV['is_rake'].empty?
|
198
208
|
)
|
199
|
-
Annotate.eager_load(options)
|
209
|
+
Annotate.eager_load(options) if Annotate.include_models?
|
200
210
|
|
201
211
|
AnnotateModels.send(target_action, options) if Annotate.include_models?
|
202
212
|
AnnotateRoutes.send(target_action, options) if Annotate.include_routes?
|
data/lib/annotate.rb
CHANGED
@@ -30,9 +30,10 @@ module Annotate
|
|
30
30
|
:show_indexes, :simple_indexes, :include_version, :exclude_tests,
|
31
31
|
:exclude_fixtures, :exclude_factories, :ignore_model_sub_dir,
|
32
32
|
:format_bare, :format_rdoc, :format_markdown, :sort, :force, :trace,
|
33
|
-
:timestamp, :exclude_serializers, :classified_sort,
|
33
|
+
:timestamp, :exclude_serializers, :classified_sort,
|
34
|
+
:show_foreign_keys, :show_complete_foreign_keys,
|
34
35
|
:exclude_scaffolds, :exclude_controllers, :exclude_helpers,
|
35
|
-
:exclude_sti_subclasses, :ignore_unknown_models
|
36
|
+
:exclude_sti_subclasses, :ignore_unknown_models, :with_comment
|
36
37
|
].freeze
|
37
38
|
OTHER_OPTIONS = [
|
38
39
|
:ignore_columns, :skip_on_db_migrate, :wrapper_open, :wrapper_close,
|
@@ -105,7 +106,7 @@ module Annotate
|
|
105
106
|
end
|
106
107
|
|
107
108
|
def self.skip_on_migration?
|
108
|
-
ENV['skip_on_db_migrate'] =~ TRUE_RE
|
109
|
+
ENV['ANNOTATE_SKIP_ON_DB_MIGRATE'] =~ TRUE_RE || ENV['skip_on_db_migrate'] =~ TRUE_RE
|
109
110
|
end
|
110
111
|
|
111
112
|
def self.include_routes?
|
@@ -113,7 +114,7 @@ module Annotate
|
|
113
114
|
end
|
114
115
|
|
115
116
|
def self.include_models?
|
116
|
-
|
117
|
+
ENV['routes'] !~ TRUE_RE
|
117
118
|
end
|
118
119
|
|
119
120
|
def self.loaded_tasks=(val)
|
@@ -168,7 +169,7 @@ module Annotate
|
|
168
169
|
require 'rake/dsl_definition'
|
169
170
|
rescue StandardError => e
|
170
171
|
# We might just be on an old version of Rake...
|
171
|
-
puts e.message
|
172
|
+
$stderr.puts e.message
|
172
173
|
exit e.status_code
|
173
174
|
end
|
174
175
|
require 'rake'
|
@@ -12,6 +12,8 @@ module AnnotateModels
|
|
12
12
|
PREFIX_MD = '## Schema Information'.freeze
|
13
13
|
END_MARK = '== Schema Information End'.freeze
|
14
14
|
|
15
|
+
SKIP_ANNOTATION_PREFIX = '# -\*- SkipSchemaAnnotations'.freeze
|
16
|
+
|
15
17
|
MATCHED_TYPES = %w(test fixture factory serializer scaffold controller helper).freeze
|
16
18
|
|
17
19
|
# File.join for windows reverse bar compat?
|
@@ -65,12 +67,27 @@ module AnnotateModels
|
|
65
67
|
# Don't show default value for these column types
|
66
68
|
NO_DEFAULT_COL_TYPES = %w(json jsonb hstore).freeze
|
67
69
|
|
70
|
+
INDEX_CLAUSES = {
|
71
|
+
unique: {
|
72
|
+
default: 'UNIQUE',
|
73
|
+
markdown: '_unique_'
|
74
|
+
},
|
75
|
+
where: {
|
76
|
+
default: 'WHERE',
|
77
|
+
markdown: '_where_'
|
78
|
+
},
|
79
|
+
using: {
|
80
|
+
default: 'USING',
|
81
|
+
markdown: '_using_'
|
82
|
+
}
|
83
|
+
}.freeze
|
84
|
+
|
68
85
|
class << self
|
69
86
|
def annotate_pattern(options = {})
|
70
87
|
if options[:wrapper_open]
|
71
|
-
return /(
|
88
|
+
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)*/
|
72
89
|
end
|
73
|
-
|
90
|
+
/^(\n|\r\n)?# (?:#{COMPAT_PREFIX}|#{COMPAT_PREFIX_MD}).*?(\n|\r\n)(#.*(\n|\r\n))*(\n|\r\n)*/
|
74
91
|
end
|
75
92
|
|
76
93
|
def model_dir
|
@@ -127,6 +144,8 @@ module AnnotateModels
|
|
127
144
|
File.join(root_directory, FACTORY_GIRL_SPEC_DIR, "%MODEL_NAME%_factory.rb"), # (old style)
|
128
145
|
File.join(root_directory, FACTORY_GIRL_TEST_DIR, "%TABLE_NAME%.rb"), # (new style)
|
129
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)
|
130
149
|
File.join(root_directory, FABRICATORS_TEST_DIR, "%MODEL_NAME%_fabricator.rb"),
|
131
150
|
File.join(root_directory, FABRICATORS_SPEC_DIR, "%MODEL_NAME%_fabricator.rb")
|
132
151
|
]
|
@@ -195,8 +214,8 @@ module AnnotateModels
|
|
195
214
|
return indexes if indexes.any? || !klass.table_name_prefix
|
196
215
|
|
197
216
|
# Try to search the table without prefix
|
198
|
-
table_name.to_s.
|
199
|
-
klass.connection.indexes(
|
217
|
+
table_name_without_prefix = table_name.to_s.sub(klass.table_name_prefix, '')
|
218
|
+
klass.connection.indexes(table_name_without_prefix)
|
200
219
|
end
|
201
220
|
|
202
221
|
# Use the column information in an ActiveRecord class
|
@@ -207,11 +226,7 @@ module AnnotateModels
|
|
207
226
|
info = "# #{header}\n"
|
208
227
|
info << get_schema_header_text(klass, options)
|
209
228
|
|
210
|
-
max_size = klass
|
211
|
-
with_comment = options[:with_comment] && klass.columns.first.respond_to?(:comment)
|
212
|
-
max_size = klass.columns.map{|col| col.name.size + col.comment.size }.max || 0 if with_comment
|
213
|
-
max_size += 2 if with_comment
|
214
|
-
max_size += options[:format_rdoc] ? 5 : 1
|
229
|
+
max_size = max_schema_info_width(klass, options)
|
215
230
|
md_names_overhead = 6
|
216
231
|
md_type_allowance = 18
|
217
232
|
bare_type_allowance = 16
|
@@ -232,7 +247,7 @@ module AnnotateModels
|
|
232
247
|
cols = cols.sort_by(&:name) if options[:sort]
|
233
248
|
cols = classified_sort(cols) if options[:classified_sort]
|
234
249
|
cols.each do |col|
|
235
|
-
col_type = (col
|
250
|
+
col_type = get_col_type(col)
|
236
251
|
attrs = []
|
237
252
|
attrs << "default(#{schema_default(klass, col)})" unless col.default.nil? || hide_default?(col_type, options)
|
238
253
|
attrs << 'unsigned' if col.respond_to?(:unsigned?) && col.unsigned?
|
@@ -274,7 +289,7 @@ module AnnotateModels
|
|
274
289
|
end
|
275
290
|
end
|
276
291
|
end
|
277
|
-
col_name = if
|
292
|
+
col_name = if with_comments?(klass, options) && col.comment
|
278
293
|
"#{col.name}(#{col.comment})"
|
279
294
|
else
|
280
295
|
col.name
|
@@ -337,15 +352,83 @@ module AnnotateModels
|
|
337
352
|
max_size = indexes.collect{|index| index.name.size}.max + 1
|
338
353
|
indexes.sort_by(&:name).each do |index|
|
339
354
|
index_info << if options[:format_markdown]
|
340
|
-
|
355
|
+
final_index_string_in_markdown(index)
|
341
356
|
else
|
342
|
-
|
357
|
+
final_index_string(index, max_size)
|
343
358
|
end
|
344
359
|
end
|
345
360
|
|
346
361
|
index_info
|
347
362
|
end
|
348
363
|
|
364
|
+
def get_col_type(col)
|
365
|
+
if col.respond_to?(:bigint?) && col.bigint?
|
366
|
+
'bigint'
|
367
|
+
else
|
368
|
+
(col.type || col.sql_type).to_s
|
369
|
+
end
|
370
|
+
end
|
371
|
+
|
372
|
+
def index_columns_info(index)
|
373
|
+
Array(index.columns).map do |col|
|
374
|
+
if index.try(:orders) && index.orders[col.to_s]
|
375
|
+
"#{col} #{index.orders[col.to_s].upcase}"
|
376
|
+
else
|
377
|
+
col.to_s.gsub("\r", '\r').gsub("\n", '\n')
|
378
|
+
end
|
379
|
+
end
|
380
|
+
end
|
381
|
+
|
382
|
+
def index_unique_info(index, format = :default)
|
383
|
+
index.unique ? " #{INDEX_CLAUSES[:unique][format]}" : ''
|
384
|
+
end
|
385
|
+
|
386
|
+
def index_where_info(index, format = :default)
|
387
|
+
value = index.try(:where).try(:to_s)
|
388
|
+
if value.blank?
|
389
|
+
''
|
390
|
+
else
|
391
|
+
" #{INDEX_CLAUSES[:where][format]} #{value}"
|
392
|
+
end
|
393
|
+
end
|
394
|
+
|
395
|
+
def index_using_info(index, format = :default)
|
396
|
+
value = index.try(:using) && index.using.try(:to_sym)
|
397
|
+
if !value.blank? && value != :btree
|
398
|
+
" #{INDEX_CLAUSES[:using][format]} #{value}"
|
399
|
+
else
|
400
|
+
''
|
401
|
+
end
|
402
|
+
end
|
403
|
+
|
404
|
+
def final_index_string_in_markdown(index)
|
405
|
+
details = sprintf(
|
406
|
+
"%s%s%s",
|
407
|
+
index_unique_info(index, :markdown),
|
408
|
+
index_where_info(index, :markdown),
|
409
|
+
index_using_info(index, :markdown)
|
410
|
+
).strip
|
411
|
+
details = " (#{details})" unless details.blank?
|
412
|
+
|
413
|
+
sprintf(
|
414
|
+
"# * `%s`%s:\n# * **`%s`**\n",
|
415
|
+
index.name,
|
416
|
+
details,
|
417
|
+
index_columns_info(index).join("`**\n# * **`")
|
418
|
+
)
|
419
|
+
end
|
420
|
+
|
421
|
+
def final_index_string(index, max_size)
|
422
|
+
sprintf(
|
423
|
+
"# %-#{max_size}.#{max_size}s %s%s%s%s",
|
424
|
+
index.name,
|
425
|
+
"(#{index_columns_info(index).join(',')})",
|
426
|
+
index_unique_info(index),
|
427
|
+
index_where_info(index),
|
428
|
+
index_using_info(index)
|
429
|
+
).rstrip + "\n"
|
430
|
+
end
|
431
|
+
|
349
432
|
def hide_limit?(col_type, options)
|
350
433
|
excludes =
|
351
434
|
if options[:hide_limit_column_types].blank?
|
@@ -417,7 +500,7 @@ module AnnotateModels
|
|
417
500
|
def annotate_one_file(file_name, info_block, position, options = {})
|
418
501
|
if File.exist?(file_name)
|
419
502
|
old_content = File.read(file_name)
|
420
|
-
return false if old_content =~ /#
|
503
|
+
return false if old_content =~ /#{SKIP_ANNOTATION_PREFIX}.*\n/
|
421
504
|
|
422
505
|
# Ignore the Schema version line because it changes with each migration
|
423
506
|
header_pattern = /(^# Table name:.*?\n(#.*[\r]?\n)*[\r]?)/
|
@@ -428,8 +511,7 @@ module AnnotateModels
|
|
428
511
|
old_columns = old_header && old_header.scan(column_pattern).sort
|
429
512
|
new_columns = new_header && new_header.scan(column_pattern).sort
|
430
513
|
|
431
|
-
|
432
|
-
magic_comments = old_content.scan(magic_comment_matcher).flatten.compact
|
514
|
+
magic_comments_block = magic_comments_as_string(old_content)
|
433
515
|
|
434
516
|
if old_columns == new_columns && !options[:force]
|
435
517
|
return false
|
@@ -447,13 +529,13 @@ module AnnotateModels
|
|
447
529
|
# if there *was* no old schema info (no substitution happened) or :force was passed,
|
448
530
|
# we simply need to insert it in correct position
|
449
531
|
if new_content == old_content || options[:force]
|
450
|
-
old_content.
|
532
|
+
old_content.gsub!(magic_comment_matcher, '')
|
451
533
|
old_content.sub!(annotate_pattern(options), '')
|
452
534
|
|
453
535
|
new_content = if %w(after bottom).include?(options[position].to_s)
|
454
|
-
|
536
|
+
magic_comments_block + (old_content.rstrip + "\n\n" + wrapped_info_block)
|
455
537
|
else
|
456
|
-
|
538
|
+
magic_comments_block + wrapped_info_block + "\n" + old_content
|
457
539
|
end
|
458
540
|
end
|
459
541
|
|
@@ -465,9 +547,25 @@ module AnnotateModels
|
|
465
547
|
end
|
466
548
|
end
|
467
549
|
|
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))/)
|
552
|
+
end
|
553
|
+
|
554
|
+
def magic_comments_as_string(content)
|
555
|
+
magic_comments = content.scan(magic_comment_matcher).flatten.compact
|
556
|
+
|
557
|
+
if magic_comments.any?
|
558
|
+
magic_comments.join + "\n"
|
559
|
+
else
|
560
|
+
''
|
561
|
+
end
|
562
|
+
end
|
563
|
+
|
468
564
|
def remove_annotation_of_file(file_name, options = {})
|
469
565
|
if File.exist?(file_name)
|
470
566
|
content = File.read(file_name)
|
567
|
+
return false if content =~ /#{SKIP_ANNOTATION_PREFIX}.*\n/
|
568
|
+
|
471
569
|
wrapper_open = options[:wrapper_open] ? "# #{options[:wrapper_open]}\n" : ''
|
472
570
|
content.sub!(/(#{wrapper_open})?#{annotate_pattern(options)}/, '')
|
473
571
|
|
@@ -542,8 +640,8 @@ module AnnotateModels
|
|
542
640
|
end
|
543
641
|
end
|
544
642
|
rescue StandardError => e
|
545
|
-
puts "Unable to annotate #{file}: #{e.message}"
|
546
|
-
puts "\t" + e.backtrace.join("\n\t") if options[:trace]
|
643
|
+
$stderr.puts "Unable to annotate #{file}: #{e.message}"
|
644
|
+
$stderr.puts "\t" + e.backtrace.join("\n\t") if options[:trace]
|
547
645
|
end
|
548
646
|
|
549
647
|
annotated
|
@@ -559,35 +657,53 @@ module AnnotateModels
|
|
559
657
|
# of model files from root dir. Otherwise we take all the model files
|
560
658
|
# in the model_dir directory.
|
561
659
|
def get_model_files(options)
|
562
|
-
|
563
|
-
unless options[:is_rake]
|
564
|
-
models = ARGV.dup.reject { |m| m.match(/^(.*)=/) }
|
565
|
-
end
|
660
|
+
model_files = []
|
566
661
|
|
567
|
-
|
568
|
-
|
569
|
-
|
570
|
-
|
571
|
-
|
572
|
-
|
573
|
-
|
574
|
-
|
575
|
-
|
576
|
-
|
577
|
-
|
578
|
-
|
579
|
-
end
|
580
|
-
rescue SystemCallError
|
581
|
-
puts "No models found in directory '#{model_dir.join("', '")}'."
|
582
|
-
puts "Either specify models on the command line, or use the --model-dir option."
|
583
|
-
puts "Call 'annotate --help' for more info."
|
584
|
-
exit 1
|
662
|
+
model_files = list_model_files_from_argument unless options[:is_rake]
|
663
|
+
|
664
|
+
return model_files unless model_files.empty?
|
665
|
+
|
666
|
+
model_dir.each do |dir|
|
667
|
+
Dir.chdir(dir) do
|
668
|
+
list = if options[:ignore_model_sub_dir]
|
669
|
+
Dir["*.rb"].map { |f| [dir, f] }
|
670
|
+
else
|
671
|
+
Dir["**/*.rb"].reject { |f| f["concerns/"] }.map { |f| [dir, f] }
|
672
|
+
end
|
673
|
+
model_files.concat(list)
|
585
674
|
end
|
586
675
|
end
|
587
676
|
|
588
|
-
|
677
|
+
model_files
|
678
|
+
rescue SystemCallError
|
679
|
+
$stderr.puts "No models found in directory '#{model_dir.join("', '")}'."
|
680
|
+
$stderr.puts "Either specify models on the command line, or use the --model-dir option."
|
681
|
+
$stderr.puts "Call 'annotate --help' for more info."
|
682
|
+
exit 1
|
589
683
|
end
|
590
684
|
|
685
|
+
def list_model_files_from_argument
|
686
|
+
return [] if ARGV.empty?
|
687
|
+
|
688
|
+
specified_files = ARGV.map { |file| File.expand_path(file) }
|
689
|
+
|
690
|
+
model_files = model_dir.flat_map do |dir|
|
691
|
+
absolute_dir_path = File.expand_path(dir)
|
692
|
+
specified_files
|
693
|
+
.find_all { |file| file.start_with?(absolute_dir_path) }
|
694
|
+
.map { |file| [dir, file.sub("#{absolute_dir_path}/", '')] }
|
695
|
+
end
|
696
|
+
|
697
|
+
if model_files.size != specified_files.size
|
698
|
+
puts "The specified file could not be found in directory '#{model_dir.join("', '")}'."
|
699
|
+
puts "Call 'annotate --help' for more info."
|
700
|
+
exit 1
|
701
|
+
end
|
702
|
+
|
703
|
+
model_files
|
704
|
+
end
|
705
|
+
private :list_model_files_from_argument
|
706
|
+
|
591
707
|
# Retrieve the classes belonging to the model names we're asked to process
|
592
708
|
# Check for namespaced models in subdirectories as well as models
|
593
709
|
# in subdirectories without namespacing.
|
@@ -595,11 +711,11 @@ module AnnotateModels
|
|
595
711
|
model_path = file.gsub(/\.rb$/, '')
|
596
712
|
model_dir.each { |dir| model_path = model_path.gsub(/^#{dir}/, '').gsub(/^\//, '') }
|
597
713
|
begin
|
598
|
-
get_loaded_model(model_path) || raise(BadModelFileError.new)
|
714
|
+
get_loaded_model(model_path, file) || raise(BadModelFileError.new)
|
599
715
|
rescue LoadError
|
600
716
|
# this is for non-rails projects, which don't get Rails auto-require magic
|
601
717
|
file_path = File.expand_path(file)
|
602
|
-
if File.file?(file_path) &&
|
718
|
+
if File.file?(file_path) && Kernel.require(file_path)
|
603
719
|
retry
|
604
720
|
elsif model_path =~ /\//
|
605
721
|
model_path = model_path.split('/')[1..-1].join('/').to_s
|
@@ -610,10 +726,26 @@ module AnnotateModels
|
|
610
726
|
end
|
611
727
|
end
|
612
728
|
|
729
|
+
# Retrieve loaded model class
|
730
|
+
def get_loaded_model(model_path, file)
|
731
|
+
loaded_model_class = get_loaded_model_by_path(model_path)
|
732
|
+
return loaded_model_class if loaded_model_class
|
733
|
+
|
734
|
+
# We cannot get loaded model when `model_path` is loaded by Rails
|
735
|
+
# auto_load/eager_load paths. Try all possible model paths one by one.
|
736
|
+
absolute_file = File.expand_path(file)
|
737
|
+
model_paths =
|
738
|
+
$LOAD_PATH.select { |path| absolute_file.include?(path) }
|
739
|
+
.map { |path| absolute_file.sub(path, '').sub(/\.rb$/, '').sub(/^\//, '') }
|
740
|
+
model_paths
|
741
|
+
.map { |path| get_loaded_model_by_path(path) }
|
742
|
+
.find { |loaded_model| !loaded_model.nil? }
|
743
|
+
end
|
744
|
+
|
613
745
|
# Retrieve loaded model class by path to the file where it's supposed to be defined.
|
614
|
-
def
|
746
|
+
def get_loaded_model_by_path(model_path)
|
615
747
|
ActiveSupport::Inflector.constantize(ActiveSupport::Inflector.camelize(model_path))
|
616
|
-
rescue
|
748
|
+
rescue StandardError, LoadError
|
617
749
|
# Revert to the old way but it is not really robust
|
618
750
|
ObjectSpace.each_object(::Class)
|
619
751
|
.select do |c|
|
@@ -624,10 +756,15 @@ module AnnotateModels
|
|
624
756
|
end
|
625
757
|
|
626
758
|
def parse_options(options = {})
|
627
|
-
self.model_dir = options[:model_dir] if options[:model_dir]
|
759
|
+
self.model_dir = split_model_dir(options[:model_dir]) if options[:model_dir]
|
628
760
|
self.root_dir = options[:root_dir] if options[:root_dir]
|
629
761
|
end
|
630
762
|
|
763
|
+
def split_model_dir(option_value)
|
764
|
+
option_value = option_value.is_a?(Array) ? option_value : option_value.split(',')
|
765
|
+
option_value.map(&:strip).reject(&:empty?)
|
766
|
+
end
|
767
|
+
|
631
768
|
# We're passed a name of things that might be
|
632
769
|
# ActiveRecord models. If we can find the class, and
|
633
770
|
# if its a subclass of ActiveRecord::Base,
|
@@ -655,7 +792,7 @@ module AnnotateModels
|
|
655
792
|
|
656
793
|
def annotate_model_file(annotated, file, header, options)
|
657
794
|
begin
|
658
|
-
return false if /#
|
795
|
+
return false if /#{SKIP_ANNOTATION_PREFIX}.*/ =~ (File.exist?(file) ? File.read(file) : '')
|
659
796
|
klass = get_model_class(file)
|
660
797
|
do_annotate = klass &&
|
661
798
|
klass < ActiveRecord::Base &&
|
@@ -666,12 +803,12 @@ module AnnotateModels
|
|
666
803
|
annotated.concat(annotate(klass, file, header, options)) if do_annotate
|
667
804
|
rescue BadModelFileError => e
|
668
805
|
unless options[:ignore_unknown_models]
|
669
|
-
puts "Unable to annotate #{file}: #{e.message}"
|
670
|
-
puts "\t" + e.backtrace.join("\n\t") if options[:trace]
|
806
|
+
$stderr.puts "Unable to annotate #{file}: #{e.message}"
|
807
|
+
$stderr.puts "\t" + e.backtrace.join("\n\t") if options[:trace]
|
671
808
|
end
|
672
809
|
rescue StandardError => e
|
673
|
-
puts "Unable to annotate #{file}: #{e.message}"
|
674
|
-
puts "\t" + e.backtrace.join("\n\t") if options[:trace]
|
810
|
+
$stderr.puts "Unable to annotate #{file}: #{e.message}"
|
811
|
+
$stderr.puts "\t" + e.backtrace.join("\n\t") if options[:trace]
|
675
812
|
end
|
676
813
|
end
|
677
814
|
|
@@ -701,8 +838,8 @@ module AnnotateModels
|
|
701
838
|
end
|
702
839
|
deannotated << klass if deannotated_klass
|
703
840
|
rescue StandardError => e
|
704
|
-
puts "Unable to deannotate #{File.join(file)}: #{e.message}"
|
705
|
-
puts "\t" + e.backtrace.join("\n\t") if options[:trace]
|
841
|
+
$stderr.puts "Unable to deannotate #{File.join(file)}: #{e.message}"
|
842
|
+
$stderr.puts "\t" + e.backtrace.join("\n\t") if options[:trace]
|
706
843
|
end
|
707
844
|
end
|
708
845
|
puts "Removed annotations from: #{deannotated.join(', ')}"
|
@@ -737,13 +874,26 @@ module AnnotateModels
|
|
737
874
|
([id] << rest_cols << timestamps << associations).flatten.compact
|
738
875
|
end
|
739
876
|
|
740
|
-
|
741
|
-
|
742
|
-
|
743
|
-
|
744
|
-
|
745
|
-
|
746
|
-
|
877
|
+
private
|
878
|
+
|
879
|
+
def with_comments?(klass, options)
|
880
|
+
options[:with_comment] &&
|
881
|
+
klass.columns.first.respond_to?(:comment) &&
|
882
|
+
klass.columns.any? { |col| !col.comment.nil? }
|
883
|
+
end
|
884
|
+
|
885
|
+
def max_schema_info_width(klass, options)
|
886
|
+
if with_comments?(klass, options)
|
887
|
+
max_size = klass.columns.map do |column|
|
888
|
+
column.name.size + (column.comment ? column.comment.size : 0)
|
889
|
+
end.max || 0
|
890
|
+
max_size += 2
|
891
|
+
else
|
892
|
+
max_size = klass.column_names.map(&:size).max
|
893
|
+
end
|
894
|
+
max_size += options[:format_rdoc] ? 5 : 1
|
895
|
+
|
896
|
+
max_size
|
747
897
|
end
|
748
898
|
end
|
749
899
|
|
@@ -1,3 +1,5 @@
|
|
1
|
+
# rubocop:disable Metrics/ModuleLength
|
2
|
+
|
1
3
|
# == Annotate Routes
|
2
4
|
#
|
3
5
|
# Based on:
|
@@ -36,7 +38,18 @@ module AnnotateRoutes
|
|
36
38
|
def header(options = {})
|
37
39
|
routes_map = app_routes_map(options)
|
38
40
|
|
39
|
-
|
41
|
+
magic_comments_map, routes_map = extract_magic_comments_from_array(routes_map)
|
42
|
+
|
43
|
+
out = []
|
44
|
+
|
45
|
+
magic_comments_map.each do |magic_comment|
|
46
|
+
out << magic_comment
|
47
|
+
end
|
48
|
+
out << '' if magic_comments_map.any?
|
49
|
+
|
50
|
+
out += ["# #{options[:wrapper_open]}"] if options[:wrapper_open]
|
51
|
+
|
52
|
+
out += ["# #{options[:format_markdown] ? PREFIX_MD : PREFIX}" + (options[:timestamp] ? " (Updated #{Time.now.strftime('%Y-%m-%d %H:%M')})" : '')]
|
40
53
|
out += ['#']
|
41
54
|
return out if routes_map.size.zero?
|
42
55
|
|
@@ -51,35 +64,56 @@ module AnnotateRoutes
|
|
51
64
|
out += ["# #{content(routes_map[0], maxs, options)}"]
|
52
65
|
end
|
53
66
|
|
54
|
-
out
|
67
|
+
out += routes_map[1..-1].map { |line| "# #{content(options[:format_markdown] ? line.split(' ') : line, maxs, options)}" }
|
68
|
+
out += ["# #{options[:wrapper_close]}"] if options[:wrapper_close]
|
69
|
+
|
70
|
+
out
|
55
71
|
end
|
56
72
|
|
57
73
|
def do_annotations(options = {})
|
58
74
|
return unless routes_exists?
|
59
75
|
existing_text = File.read(routes_file)
|
60
76
|
|
61
|
-
if
|
77
|
+
if rewrite_contents_with_header(existing_text, header(options), options)
|
62
78
|
puts "#{routes_file} annotated."
|
63
79
|
end
|
64
80
|
end
|
65
81
|
|
66
|
-
def remove_annotations(
|
82
|
+
def remove_annotations(_options={})
|
67
83
|
return unless routes_exists?
|
68
84
|
existing_text = File.read(routes_file)
|
69
85
|
content, where_header_found = strip_annotations(existing_text)
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
if write_contents(existing_text, content, options)
|
86
|
+
new_content = strip_on_removal(content, where_header_found)
|
87
|
+
if rewrite_contents(existing_text, new_content)
|
74
88
|
puts "Removed annotations from #{routes_file}."
|
75
89
|
end
|
76
90
|
end
|
77
91
|
end
|
78
92
|
|
79
|
-
|
93
|
+
def self.magic_comment_matcher
|
94
|
+
Regexp.new(/(^#\s*encoding:.*)|(^# coding:.*)|(^# -\*- coding:.*)|(^# -\*- encoding\s?:.*)|(^#\s*frozen_string_literal:.+)|(^# -\*- frozen_string_literal\s*:.+-\*-)/)
|
95
|
+
end
|
96
|
+
|
97
|
+
# @param [Array<String>] content
|
98
|
+
# @return [Array<String>] all found magic comments
|
99
|
+
# @return [Array<String>] content without magic comments
|
100
|
+
def self.extract_magic_comments_from_array(content_array)
|
101
|
+
magic_comments = []
|
102
|
+
new_content = []
|
103
|
+
|
104
|
+
content_array.map do |row|
|
105
|
+
if row =~ magic_comment_matcher
|
106
|
+
magic_comments << row.strip
|
107
|
+
else
|
108
|
+
new_content << row
|
109
|
+
end
|
110
|
+
end
|
111
|
+
|
112
|
+
[magic_comments, new_content]
|
113
|
+
end
|
80
114
|
|
81
115
|
def self.app_routes_map(options)
|
82
|
-
routes_map = `rake routes`.split(/\n/, -1)
|
116
|
+
routes_map = `rake routes`.chomp("\n").split(/\n/, -1)
|
83
117
|
|
84
118
|
# In old versions of Rake, the first line of output was the cwd. Not so
|
85
119
|
# much in newer ones. We ditch that line if it exists, and if not, we
|
@@ -106,7 +140,22 @@ module AnnotateRoutes
|
|
106
140
|
routes_exists
|
107
141
|
end
|
108
142
|
|
109
|
-
|
143
|
+
# @param [String, Array<String>]
|
144
|
+
def self.rewrite_contents(existing_text, new_content)
|
145
|
+
# Make sure we end on a trailing newline.
|
146
|
+
new_content << '' unless new_content.last == ''
|
147
|
+
new_text = new_content.join("\n")
|
148
|
+
|
149
|
+
if existing_text == new_text
|
150
|
+
puts "#{routes_file} unchanged."
|
151
|
+
false
|
152
|
+
else
|
153
|
+
File.open(routes_file, 'wb') { |f| f.puts(new_text) }
|
154
|
+
true
|
155
|
+
end
|
156
|
+
end
|
157
|
+
|
158
|
+
def self.rewrite_contents_with_header(existing_text, header, options = {})
|
110
159
|
content, where_header_found = strip_annotations(existing_text)
|
111
160
|
new_content = annotate_routes(header, content, where_header_found, options)
|
112
161
|
|
@@ -124,9 +173,11 @@ module AnnotateRoutes
|
|
124
173
|
end
|
125
174
|
|
126
175
|
def self.annotate_routes(header, content, where_header_found, options = {})
|
176
|
+
magic_comments_map, content = extract_magic_comments_from_array(content)
|
127
177
|
if %w(before top).include?(options[:position_in_routes])
|
128
178
|
header = header << '' if content.first != ''
|
129
|
-
|
179
|
+
magic_comments_map << '' if magic_comments_map.any?
|
180
|
+
new_content = magic_comments_map + header + content
|
130
181
|
else
|
131
182
|
# Ensure we have adequate trailing newlines at the end of the file to
|
132
183
|
# ensure a blank line separating the content from the annotation.
|
@@ -136,7 +187,7 @@ module AnnotateRoutes
|
|
136
187
|
# the spacer we put in the first time around.
|
137
188
|
content.shift if where_header_found == :before && content.first == ''
|
138
189
|
|
139
|
-
new_content = content + header
|
190
|
+
new_content = magic_comments_map + content + header
|
140
191
|
end
|
141
192
|
|
142
193
|
new_content
|
@@ -156,7 +207,7 @@ module AnnotateRoutes
|
|
156
207
|
content.split(/\n/, -1).each_with_index do |line, line_number|
|
157
208
|
if mode == :header && line !~ /\s*#/
|
158
209
|
mode = :content
|
159
|
-
|
210
|
+
real_content << line unless line.blank?
|
160
211
|
elsif mode == :content
|
161
212
|
if line =~ /^\s*#\s*== Route.*$/
|
162
213
|
header_found_at = line_number + 1 # index start's at 0
|
data/lib/annotate/version.rb
CHANGED
@@ -1,8 +1,10 @@
|
|
1
|
+
require 'annotate'
|
2
|
+
|
1
3
|
module Annotate
|
2
4
|
module Generators
|
3
5
|
class InstallGenerator < Rails::Generators::Base
|
4
6
|
desc 'Copy annotate_models rakefiles for automatic annotation'
|
5
|
-
source_root File.expand_path('
|
7
|
+
source_root File.expand_path('templates', __dir__)
|
6
8
|
|
7
9
|
# copy rake tasks
|
8
10
|
def copy_tasks
|
@@ -47,6 +47,7 @@ task annotate_models: :environment do
|
|
47
47
|
options[:ignore_routes] = ENV.fetch('ignore_routes', nil)
|
48
48
|
options[:hide_limit_column_types] = Annotate.fallback(ENV['hide_limit_column_types'], '')
|
49
49
|
options[:hide_default_column_types] = Annotate.fallback(ENV['hide_default_column_types'], '')
|
50
|
+
options[:with_comment] = Annotate.fallback(ENV['with_comment'], '')
|
50
51
|
|
51
52
|
AnnotateModels.do_annotations(options)
|
52
53
|
end
|
@@ -8,6 +8,8 @@ task :annotate_routes => :environment do
|
|
8
8
|
options[:position_in_routes] = Annotate.fallback(ENV['position_in_routes'], ENV['position'])
|
9
9
|
options[:ignore_routes] = Annotate.fallback(ENV['ignore_routes'], nil)
|
10
10
|
options[:require] = ENV['require'] ? ENV['require'].split(',') : []
|
11
|
+
options[:wrapper_open] = Annotate.fallback(ENV['wrapper_open'], ENV['wrapper'])
|
12
|
+
options[:wrapper_close] = Annotate.fallback(ENV['wrapper_close'], ENV['wrapper'])
|
11
13
|
AnnotateRoutes.do_annotations(options)
|
12
14
|
end
|
13
15
|
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: annotate
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 2.7.
|
4
|
+
version: 2.7.3
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Alex Chaffee
|
@@ -12,7 +12,7 @@ authors:
|
|
12
12
|
autorequire:
|
13
13
|
bindir: bin
|
14
14
|
cert_chain: []
|
15
|
-
date:
|
15
|
+
date: 2018-04-21 00:00:00.000000000 Z
|
16
16
|
dependencies:
|
17
17
|
- !ruby/object:Gem::Dependency
|
18
18
|
name: rake
|
@@ -101,7 +101,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
101
101
|
requirements:
|
102
102
|
- - ">="
|
103
103
|
- !ruby/object:Gem::Version
|
104
|
-
version:
|
104
|
+
version: 2.2.0
|
105
105
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
106
106
|
requirements:
|
107
107
|
- - ">="
|
@@ -109,7 +109,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
109
109
|
version: '0'
|
110
110
|
requirements: []
|
111
111
|
rubyforge_project: annotate
|
112
|
-
rubygems_version: 2.6
|
112
|
+
rubygems_version: 2.7.6
|
113
113
|
signing_key:
|
114
114
|
specification_version: 4
|
115
115
|
summary: Annotates Rails Models, routes, fixtures, and others based on the database
|