annotate 2.7.2 → 2.7.3
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.
- 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
|