annotate 2.7.5 → 3.2.0
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 +4 -4
- data/{AUTHORS.rdoc → AUTHORS.md} +2 -2
- data/CHANGELOG.md +326 -0
- data/{README.rdoc → README.md} +154 -100
- data/RELEASE.md +19 -0
- data/annotate.gemspec +11 -27
- data/bin/annotate +7 -191
- data/lib/annotate/annotate_models/file_patterns.rb +127 -0
- data/lib/annotate/annotate_models.rb +183 -188
- data/lib/annotate/annotate_routes/header_generator.rb +113 -0
- data/lib/annotate/annotate_routes/helpers.rb +69 -0
- data/lib/annotate/annotate_routes.rb +69 -200
- data/lib/annotate/constants.rb +38 -0
- data/lib/annotate/helpers.rb +30 -0
- data/lib/annotate/parser.rb +303 -0
- data/lib/annotate/version.rb +1 -1
- data/lib/annotate.rb +23 -82
- data/lib/generators/annotate/templates/auto_annotate_models.rake +45 -41
- data/lib/tasks/annotate_models.rake +37 -35
- data/lib/tasks/annotate_models_migrate.rake +17 -4
- data/lib/tasks/annotate_routes.rake +12 -6
- data/potato.md +41 -0
- metadata +25 -17
- data/CHANGELOG.rdoc +0 -220
- data/TODO.rdoc +0 -11
@@ -2,9 +2,10 @@
|
|
2
2
|
|
3
3
|
require 'bigdecimal'
|
4
4
|
|
5
|
-
|
6
|
-
|
5
|
+
require 'annotate/constants'
|
6
|
+
require_relative 'annotate_models/file_patterns'
|
7
7
|
|
8
|
+
module AnnotateModels
|
8
9
|
# Annotate Models plugin use this header
|
9
10
|
COMPAT_PREFIX = '== Schema Info'.freeze
|
10
11
|
COMPAT_PREFIX_MD = '## Schema Info'.freeze
|
@@ -16,50 +17,6 @@ module AnnotateModels
|
|
16
17
|
|
17
18
|
MATCHED_TYPES = %w(test fixture factory serializer scaffold controller helper).freeze
|
18
19
|
|
19
|
-
# File.join for windows reverse bar compat?
|
20
|
-
# I dont use windows, can`t test
|
21
|
-
UNIT_TEST_DIR = File.join('test', "unit")
|
22
|
-
MODEL_TEST_DIR = File.join('test', "models") # since rails 4.0
|
23
|
-
SPEC_MODEL_DIR = File.join('spec', "models")
|
24
|
-
FIXTURE_TEST_DIR = File.join('test', "fixtures")
|
25
|
-
FIXTURE_SPEC_DIR = File.join('spec', "fixtures")
|
26
|
-
|
27
|
-
# Other test files
|
28
|
-
CONTROLLER_TEST_DIR = File.join('test', "controllers")
|
29
|
-
CONTROLLER_SPEC_DIR = File.join('spec', "controllers")
|
30
|
-
REQUEST_SPEC_DIR = File.join('spec', "requests")
|
31
|
-
ROUTING_SPEC_DIR = File.join('spec', "routing")
|
32
|
-
|
33
|
-
# Object Daddy http://github.com/flogic/object_daddy/tree/master
|
34
|
-
EXEMPLARS_TEST_DIR = File.join('test', "exemplars")
|
35
|
-
EXEMPLARS_SPEC_DIR = File.join('spec', "exemplars")
|
36
|
-
|
37
|
-
# Machinist http://github.com/notahat/machinist
|
38
|
-
BLUEPRINTS_TEST_DIR = File.join('test', "blueprints")
|
39
|
-
BLUEPRINTS_SPEC_DIR = File.join('spec', "blueprints")
|
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")
|
44
|
-
|
45
|
-
# Fabrication https://github.com/paulelliott/fabrication.git
|
46
|
-
FABRICATORS_TEST_DIR = File.join('test', "fabricators")
|
47
|
-
FABRICATORS_SPEC_DIR = File.join('spec', "fabricators")
|
48
|
-
|
49
|
-
# Serializers https://github.com/rails-api/active_model_serializers
|
50
|
-
SERIALIZERS_DIR = File.join('app', "serializers")
|
51
|
-
SERIALIZERS_TEST_DIR = File.join('test', "serializers")
|
52
|
-
SERIALIZERS_SPEC_DIR = File.join('spec', "serializers")
|
53
|
-
|
54
|
-
# Controller files
|
55
|
-
CONTROLLER_DIR = File.join('app', "controllers")
|
56
|
-
|
57
|
-
# Active admin registry files
|
58
|
-
ACTIVEADMIN_DIR = File.join('app', "admin")
|
59
|
-
|
60
|
-
# Helper files
|
61
|
-
HELPER_DIR = File.join('app', "helpers")
|
62
|
-
|
63
20
|
# Don't show limit (#) on these column types
|
64
21
|
# Example: show "integer" instead of "integer(4)"
|
65
22
|
NO_LIMIT_COL_TYPES = %w(integer bigint boolean).freeze
|
@@ -82,6 +39,8 @@ module AnnotateModels
|
|
82
39
|
}
|
83
40
|
}.freeze
|
84
41
|
|
42
|
+
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
|
43
|
+
|
85
44
|
class << self
|
86
45
|
def annotate_pattern(options = {})
|
87
46
|
if options[:wrapper_open]
|
@@ -108,83 +67,33 @@ module AnnotateModels
|
|
108
67
|
|
109
68
|
attr_writer :root_dir
|
110
69
|
|
111
|
-
def
|
112
|
-
[
|
113
|
-
|
114
|
-
File.join(root_directory, MODEL_TEST_DIR, "%MODEL_NAME%_test.rb"),
|
115
|
-
File.join(root_directory, SPEC_MODEL_DIR, "%MODEL_NAME%_spec.rb")
|
116
|
-
]
|
117
|
-
end
|
118
|
-
|
119
|
-
def fixture_files(root_directory)
|
120
|
-
[
|
121
|
-
File.join(root_directory, FIXTURE_TEST_DIR, "%TABLE_NAME%.yml"),
|
122
|
-
File.join(root_directory, FIXTURE_SPEC_DIR, "%TABLE_NAME%.yml"),
|
123
|
-
File.join(root_directory, FIXTURE_TEST_DIR, "%PLURALIZED_MODEL_NAME%.yml"),
|
124
|
-
File.join(root_directory, FIXTURE_SPEC_DIR, "%PLURALIZED_MODEL_NAME%.yml")
|
125
|
-
]
|
126
|
-
end
|
127
|
-
|
128
|
-
def scaffold_files(root_directory)
|
129
|
-
[
|
130
|
-
File.join(root_directory, CONTROLLER_TEST_DIR, "%PLURALIZED_MODEL_NAME%_controller_test.rb"),
|
131
|
-
File.join(root_directory, CONTROLLER_SPEC_DIR, "%PLURALIZED_MODEL_NAME%_controller_spec.rb"),
|
132
|
-
File.join(root_directory, REQUEST_SPEC_DIR, "%PLURALIZED_MODEL_NAME%_spec.rb"),
|
133
|
-
File.join(root_directory, ROUTING_SPEC_DIR, "%PLURALIZED_MODEL_NAME%_routing_spec.rb")
|
134
|
-
]
|
135
|
-
end
|
136
|
-
|
137
|
-
def factory_files(root_directory)
|
138
|
-
[
|
139
|
-
File.join(root_directory, EXEMPLARS_TEST_DIR, "%MODEL_NAME%_exemplar.rb"),
|
140
|
-
File.join(root_directory, EXEMPLARS_SPEC_DIR, "%MODEL_NAME%_exemplar.rb"),
|
141
|
-
File.join(root_directory, BLUEPRINTS_TEST_DIR, "%MODEL_NAME%_blueprint.rb"),
|
142
|
-
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)
|
149
|
-
File.join(root_directory, FABRICATORS_TEST_DIR, "%MODEL_NAME%_fabricator.rb"),
|
150
|
-
File.join(root_directory, FABRICATORS_SPEC_DIR, "%MODEL_NAME%_fabricator.rb")
|
151
|
-
]
|
152
|
-
end
|
153
|
-
|
154
|
-
def serialize_files(root_directory)
|
155
|
-
[
|
156
|
-
File.join(root_directory, SERIALIZERS_DIR, "%MODEL_NAME%_serializer.rb"),
|
157
|
-
File.join(root_directory, SERIALIZERS_TEST_DIR, "%MODEL_NAME%_serializer_spec.rb"),
|
158
|
-
File.join(root_directory, SERIALIZERS_SPEC_DIR, "%MODEL_NAME%_serializer_spec.rb")
|
159
|
-
]
|
160
|
-
end
|
70
|
+
def skip_subdirectory_model_load
|
71
|
+
# This option is set in options[:skip_subdirectory_model_load]
|
72
|
+
# and stops the get_loaded_model method from loading a model from a subdir
|
161
73
|
|
162
|
-
|
163
|
-
|
164
|
-
when 'test' then test_files(root_directory)
|
165
|
-
when 'fixture' then fixture_files(root_directory)
|
166
|
-
when 'scaffold' then scaffold_files(root_directory)
|
167
|
-
when 'factory' then factory_files(root_directory)
|
168
|
-
when 'serializer' then serialize_files(root_directory)
|
169
|
-
when 'controller'
|
170
|
-
[File.join(root_directory, CONTROLLER_DIR, "%PLURALIZED_MODEL_NAME%_controller.rb")]
|
171
|
-
when 'admin'
|
172
|
-
[File.join(root_directory, ACTIVEADMIN_DIR, "%MODEL_NAME%.rb")]
|
173
|
-
when 'helper'
|
174
|
-
[File.join(root_directory, HELPER_DIR, "%PLURALIZED_MODEL_NAME%_helper.rb")]
|
74
|
+
if @skip_subdirectory_model_load.blank?
|
75
|
+
false
|
175
76
|
else
|
176
|
-
|
77
|
+
@skip_subdirectory_model_load
|
177
78
|
end
|
178
79
|
end
|
179
80
|
|
180
|
-
|
81
|
+
attr_writer :skip_subdirectory_model_load
|
82
|
+
|
83
|
+
def get_patterns(options, pattern_types = [])
|
181
84
|
current_patterns = []
|
182
85
|
root_dir.each do |root_directory|
|
183
86
|
Array(pattern_types).each do |pattern_type|
|
184
|
-
|
87
|
+
patterns = FilePatterns.generate(root_directory, pattern_type, options)
|
88
|
+
|
89
|
+
current_patterns += if pattern_type.to_sym == :additional_file_patterns
|
90
|
+
patterns
|
91
|
+
else
|
92
|
+
patterns.map { |p| p.sub(/^[\/]*/, '') }
|
93
|
+
end
|
185
94
|
end
|
186
95
|
end
|
187
|
-
current_patterns
|
96
|
+
current_patterns
|
188
97
|
end
|
189
98
|
|
190
99
|
# Simple quoting for the default column value
|
@@ -236,68 +145,24 @@ module AnnotateModels
|
|
236
145
|
info << "# #{ '-' * ( max_size + md_names_overhead ) } | #{'-' * md_type_allowance} | #{ '-' * 27 }\n"
|
237
146
|
end
|
238
147
|
|
239
|
-
cols =
|
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]
|
148
|
+
cols = columns(klass, options)
|
249
149
|
cols.each do |col|
|
250
150
|
col_type = get_col_type(col)
|
251
|
-
attrs =
|
252
|
-
attrs << "default(#{schema_default(klass, col)})" unless col.default.nil? || hide_default?(col_type, options)
|
253
|
-
attrs << 'unsigned' if col.respond_to?(:unsigned?) && col.unsigned?
|
254
|
-
attrs << 'not null' unless col.null
|
255
|
-
attrs << 'primary key' if klass.primary_key && (klass.primary_key.is_a?(Array) ? klass.primary_key.collect(&:to_sym).include?(col.name.to_sym) : col.name.to_sym == klass.primary_key.to_sym)
|
256
|
-
|
257
|
-
if col_type == 'decimal'
|
258
|
-
col_type << "(#{col.precision}, #{col.scale})"
|
259
|
-
elsif col_type != 'spatial'
|
260
|
-
if col.limit
|
261
|
-
if col.limit.is_a? Array
|
262
|
-
attrs << "(#{col.limit.join(', ')})"
|
263
|
-
else
|
264
|
-
col_type << "(#{col.limit})" unless hide_limit?(col_type, options)
|
265
|
-
end
|
266
|
-
end
|
267
|
-
end
|
268
|
-
|
269
|
-
# Check out if we got an array column
|
270
|
-
attrs << 'is an Array' if col.respond_to?(:array) && col.array
|
271
|
-
|
272
|
-
# Check out if we got a geometric column
|
273
|
-
# and print the type and SRID
|
274
|
-
if col.respond_to?(:geometry_type)
|
275
|
-
attrs << "#{col.geometry_type}, #{col.srid}"
|
276
|
-
elsif col.respond_to?(:geometric_type) && col.geometric_type.present?
|
277
|
-
attrs << "#{col.geometric_type.to_s.downcase}, #{col.srid}"
|
278
|
-
end
|
279
|
-
|
280
|
-
# Check if the column has indices and print "indexed" if true
|
281
|
-
# If the index includes another column, print it too.
|
282
|
-
if options[:simple_indexes] && klass.table_exists?# Check out if this column is indexed
|
283
|
-
indices = retrieve_indexes_from_table(klass)
|
284
|
-
if indices = indices.select { |ind| ind.columns.include? col.name }
|
285
|
-
indices.sort_by(&:name).each do |ind|
|
286
|
-
next if ind.columns.is_a?(String)
|
287
|
-
ind = ind.columns.reject! { |i| i == col.name }
|
288
|
-
attrs << (ind.empty? ? "indexed" : "indexed => [#{ind.join(", ")}]")
|
289
|
-
end
|
290
|
-
end
|
291
|
-
end
|
151
|
+
attrs = get_attributes(col, col_type, klass, options)
|
292
152
|
col_name = if with_comments?(klass, options) && col.comment
|
293
|
-
"#{col.name}(#{col.comment})"
|
153
|
+
"#{col.name}(#{col.comment.gsub(/\n/, "\\n")})"
|
294
154
|
else
|
295
155
|
col.name
|
296
156
|
end
|
157
|
+
|
297
158
|
if options[:format_rdoc]
|
298
159
|
info << sprintf("# %-#{max_size}.#{max_size}s<tt>%s</tt>", "*#{col_name}*::", attrs.unshift(col_type).join(", ")).rstrip + "\n"
|
160
|
+
elsif options[:format_yard]
|
161
|
+
info << sprintf("# @!attribute #{col_name}") + "\n"
|
162
|
+
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)
|
163
|
+
info << sprintf("# @return [#{ruby_class}]") + "\n"
|
299
164
|
elsif options[:format_markdown]
|
300
|
-
name_remainder = max_size - col_name.length
|
165
|
+
name_remainder = max_size - col_name.length - non_ascii_length(col_name)
|
301
166
|
type_remainder = (md_type_allowance - 2) - col_type.length
|
302
167
|
info << (sprintf("# **`%s`**%#{name_remainder}s | `%s`%#{type_remainder}s | `%s`", col_name, " ", col_type, " ", attrs.join(", ").rstrip)).gsub('``', ' ').rstrip + "\n"
|
303
168
|
else
|
@@ -464,7 +329,10 @@ module AnnotateModels
|
|
464
329
|
foreign_keys = klass.connection.foreign_keys(klass.table_name)
|
465
330
|
return '' if foreign_keys.empty?
|
466
331
|
|
467
|
-
format_name =
|
332
|
+
format_name = lambda do |fk|
|
333
|
+
return fk.options[:column] if fk.name.blank?
|
334
|
+
options[:show_complete_foreign_keys] ? fk.name : fk.name.gsub(/(?<=^fk_rails_)[0-9a-f]{10}$/, '...')
|
335
|
+
end
|
468
336
|
|
469
337
|
max_size = foreign_keys.map(&format_name).map(&:size).max + 1
|
470
338
|
foreign_keys.sort_by {|fk| [format_name.call(fk), fk.column]}.each do |fk|
|
@@ -507,7 +375,7 @@ module AnnotateModels
|
|
507
375
|
old_header = old_content.match(header_pattern).to_s
|
508
376
|
new_header = info_block.match(header_pattern).to_s
|
509
377
|
|
510
|
-
column_pattern = /^#[\t ]+[\w
|
378
|
+
column_pattern = /^#[\t ]+[\w\*\.`]+[\t ]+.+$/
|
511
379
|
old_columns = old_header && old_header.scan(column_pattern).sort
|
512
380
|
new_columns = new_header && new_header.scan(column_pattern).sort
|
513
381
|
|
@@ -526,15 +394,15 @@ module AnnotateModels
|
|
526
394
|
# need to insert it in correct position
|
527
395
|
if old_annotation.empty? || options[:force]
|
528
396
|
magic_comments_block = magic_comments_as_string(old_content)
|
529
|
-
old_content.gsub!(
|
397
|
+
old_content.gsub!(MAGIC_COMMENT_MATCHER, '')
|
530
398
|
old_content.sub!(annotate_pattern(options), '')
|
531
399
|
|
532
400
|
new_content = if %w(after bottom).include?(options[position].to_s)
|
533
401
|
magic_comments_block + (old_content.rstrip + "\n\n" + wrapped_info_block)
|
534
402
|
elsif magic_comments_block.empty?
|
535
|
-
magic_comments_block + wrapped_info_block +
|
403
|
+
magic_comments_block + wrapped_info_block + old_content.lstrip
|
536
404
|
else
|
537
|
-
magic_comments_block + "\n" + wrapped_info_block +
|
405
|
+
magic_comments_block + "\n" + wrapped_info_block + old_content.lstrip
|
538
406
|
end
|
539
407
|
else
|
540
408
|
# replace the old annotation with the new one
|
@@ -550,12 +418,8 @@ module AnnotateModels
|
|
550
418
|
true
|
551
419
|
end
|
552
420
|
|
553
|
-
def magic_comment_matcher
|
554
|
-
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))/)
|
555
|
-
end
|
556
|
-
|
557
421
|
def magic_comments_as_string(content)
|
558
|
-
magic_comments = content.scan(
|
422
|
+
magic_comments = content.scan(MAGIC_COMMENT_MATCHER).flatten.compact
|
559
423
|
|
560
424
|
if magic_comments.any?
|
561
425
|
magic_comments.join
|
@@ -581,8 +445,9 @@ module AnnotateModels
|
|
581
445
|
end
|
582
446
|
|
583
447
|
def matched_types(options)
|
584
|
-
types = MATCHED_TYPES
|
585
|
-
types << 'admin' if options[:active_admin] =~ TRUE_RE && !types.include?('admin')
|
448
|
+
types = MATCHED_TYPES.dup
|
449
|
+
types << 'admin' if options[:active_admin] =~ Annotate::Constants::TRUE_RE && !types.include?('admin')
|
450
|
+
types << 'additional_file_patterns' if options[:additional_file_patterns].present?
|
586
451
|
|
587
452
|
types
|
588
453
|
end
|
@@ -634,8 +499,11 @@ module AnnotateModels
|
|
634
499
|
end
|
635
500
|
|
636
501
|
next if options[exclusion_key]
|
637
|
-
|
502
|
+
|
503
|
+
get_patterns(options, key)
|
638
504
|
.map { |f| resolve_filename(f, model_name, table_name) }
|
505
|
+
.map { |f| expand_glob_into_files(f) }
|
506
|
+
.flatten
|
639
507
|
.each do |f|
|
640
508
|
if annotate_one_file(f, info, position_key, options_with_position(options, position_key))
|
641
509
|
annotated << f
|
@@ -731,14 +599,17 @@ module AnnotateModels
|
|
731
599
|
|
732
600
|
# Retrieve loaded model class
|
733
601
|
def get_loaded_model(model_path, file)
|
734
|
-
|
735
|
-
|
602
|
+
unless skip_subdirectory_model_load
|
603
|
+
loaded_model_class = get_loaded_model_by_path(model_path)
|
604
|
+
return loaded_model_class if loaded_model_class
|
605
|
+
end
|
736
606
|
|
737
607
|
# We cannot get loaded model when `model_path` is loaded by Rails
|
738
608
|
# auto_load/eager_load paths. Try all possible model paths one by one.
|
739
609
|
absolute_file = File.expand_path(file)
|
740
610
|
model_paths =
|
741
|
-
$LOAD_PATH.
|
611
|
+
$LOAD_PATH.map(&:to_s)
|
612
|
+
.select { |path| absolute_file.include?(path) }
|
742
613
|
.map { |path| absolute_file.sub(path, '').sub(/\.rb$/, '').sub(/^\//, '') }
|
743
614
|
model_paths
|
744
615
|
.map { |path| get_loaded_model_by_path(path) }
|
@@ -761,6 +632,7 @@ module AnnotateModels
|
|
761
632
|
def parse_options(options = {})
|
762
633
|
self.model_dir = split_model_dir(options[:model_dir]) if options[:model_dir]
|
763
634
|
self.root_dir = options[:root_dir] if options[:root_dir]
|
635
|
+
self.skip_subdirectory_model_load = options[:skip_subdirectory_model_load].present?
|
764
636
|
end
|
765
637
|
|
766
638
|
def split_model_dir(option_value)
|
@@ -793,11 +665,15 @@ module AnnotateModels
|
|
793
665
|
end
|
794
666
|
end
|
795
667
|
|
668
|
+
def expand_glob_into_files(glob)
|
669
|
+
Dir.glob(glob)
|
670
|
+
end
|
671
|
+
|
796
672
|
def annotate_model_file(annotated, file, header, options)
|
797
673
|
begin
|
798
674
|
return false if /#{SKIP_ANNOTATION_PREFIX}.*/ =~ (File.exist?(file) ? File.read(file) : '')
|
799
675
|
klass = get_model_class(file)
|
800
|
-
do_annotate = klass &&
|
676
|
+
do_annotate = klass.is_a?(Class) &&
|
801
677
|
klass < ActiveRecord::Base &&
|
802
678
|
(!options[:exclude_sti_subclasses] || !(klass.superclass < ActiveRecord::Base && klass.table_name == klass.superclass.table_name)) &&
|
803
679
|
!klass.abstract_class? &&
|
@@ -830,7 +706,7 @@ module AnnotateModels
|
|
830
706
|
model_file_name = file
|
831
707
|
deannotated_klass = true if remove_annotation_of_file(model_file_name, options)
|
832
708
|
|
833
|
-
get_patterns(matched_types(options))
|
709
|
+
get_patterns(options, matched_types(options))
|
834
710
|
.map { |f| resolve_filename(f, model_name, table_name) }
|
835
711
|
.each do |f|
|
836
712
|
if File.exist?(f)
|
@@ -886,13 +762,15 @@ module AnnotateModels
|
|
886
762
|
end
|
887
763
|
|
888
764
|
def max_schema_info_width(klass, options)
|
765
|
+
cols = columns(klass, options)
|
766
|
+
|
889
767
|
if with_comments?(klass, options)
|
890
|
-
max_size =
|
768
|
+
max_size = cols.map do |column|
|
891
769
|
column.name.size + (column.comment ? width(column.comment) : 0)
|
892
770
|
end.max || 0
|
893
771
|
max_size += 2
|
894
772
|
else
|
895
|
-
max_size =
|
773
|
+
max_size = cols.map(&:name).map(&:size).max
|
896
774
|
end
|
897
775
|
max_size += options[:format_rdoc] ? 5 : 1
|
898
776
|
|
@@ -904,13 +782,130 @@ module AnnotateModels
|
|
904
782
|
end
|
905
783
|
|
906
784
|
def width(string)
|
907
|
-
string.chars.inject(0) { |acc, elem| acc + (elem.bytesize ==
|
785
|
+
string.chars.inject(0) { |acc, elem| acc + (elem.bytesize == 3 ? 2 : 1) }
|
908
786
|
end
|
909
787
|
|
910
788
|
def mb_chars_ljust(string, length)
|
911
789
|
string = string.to_s
|
912
790
|
padding = length - width(string)
|
913
|
-
|
791
|
+
if padding > 0
|
792
|
+
string + (' ' * padding)
|
793
|
+
else
|
794
|
+
string[0..length-1]
|
795
|
+
end
|
796
|
+
end
|
797
|
+
|
798
|
+
def non_ascii_length(string)
|
799
|
+
string.to_s.chars.reject(&:ascii_only?).length
|
800
|
+
end
|
801
|
+
|
802
|
+
def map_col_type_to_ruby_classes(col_type)
|
803
|
+
case col_type
|
804
|
+
when 'integer' then Integer.to_s
|
805
|
+
when 'float' then Float.to_s
|
806
|
+
when 'decimal' then BigDecimal.to_s
|
807
|
+
when 'datetime', 'timestamp', 'time' then Time.to_s
|
808
|
+
when 'date' then Date.to_s
|
809
|
+
when 'text', 'string', 'binary', 'inet', 'uuid' then String.to_s
|
810
|
+
when 'json', 'jsonb' then Hash.to_s
|
811
|
+
when 'boolean' then 'Boolean'
|
812
|
+
end
|
813
|
+
end
|
814
|
+
|
815
|
+
def columns(klass, options)
|
816
|
+
cols = klass.columns
|
817
|
+
cols += translated_columns(klass)
|
818
|
+
|
819
|
+
if ignore_columns = options[:ignore_columns]
|
820
|
+
cols = cols.reject do |col|
|
821
|
+
col.name.match(/#{ignore_columns}/)
|
822
|
+
end
|
823
|
+
end
|
824
|
+
|
825
|
+
cols = cols.sort_by(&:name) if options[:sort]
|
826
|
+
cols = classified_sort(cols) if options[:classified_sort]
|
827
|
+
|
828
|
+
cols
|
829
|
+
end
|
830
|
+
|
831
|
+
##
|
832
|
+
# Add columns managed by the globalize gem if this gem is being used.
|
833
|
+
def translated_columns(klass)
|
834
|
+
return [] unless klass.respond_to? :translation_class
|
835
|
+
|
836
|
+
ignored_cols = ignored_translation_table_colums(klass)
|
837
|
+
klass.translation_class.columns.reject do |col|
|
838
|
+
ignored_cols.include? col.name.to_sym
|
839
|
+
end
|
840
|
+
end
|
841
|
+
|
842
|
+
##
|
843
|
+
# These are the columns that the globalize gem needs to work but
|
844
|
+
# are not necessary for the models to be displayed as annotations.
|
845
|
+
def ignored_translation_table_colums(klass)
|
846
|
+
# Construct the foreign column name in the translations table
|
847
|
+
# eg. Model: Car, foreign column name: car_id
|
848
|
+
foreign_column_name = [
|
849
|
+
klass.table_name.to_s.singularize,
|
850
|
+
'_id'
|
851
|
+
].join.to_sym
|
852
|
+
|
853
|
+
[
|
854
|
+
:id,
|
855
|
+
:created_at,
|
856
|
+
:updated_at,
|
857
|
+
:locale,
|
858
|
+
foreign_column_name
|
859
|
+
]
|
860
|
+
end
|
861
|
+
|
862
|
+
##
|
863
|
+
# Get the list of attributes that should be included in the annotation for
|
864
|
+
# a given column.
|
865
|
+
def get_attributes(column, column_type, klass, options)
|
866
|
+
attrs = []
|
867
|
+
attrs << "default(#{schema_default(klass, column)})" unless column.default.nil? || hide_default?(column_type, options)
|
868
|
+
attrs << 'unsigned' if column.respond_to?(:unsigned?) && column.unsigned?
|
869
|
+
attrs << 'not null' unless column.null
|
870
|
+
attrs << 'primary key' if klass.primary_key && (klass.primary_key.is_a?(Array) ? klass.primary_key.collect(&:to_sym).include?(column.name.to_sym) : column.name.to_sym == klass.primary_key.to_sym)
|
871
|
+
|
872
|
+
if column_type == 'decimal'
|
873
|
+
column_type << "(#{column.precision}, #{column.scale})"
|
874
|
+
elsif !%w[spatial geometry geography].include?(column_type)
|
875
|
+
if column.limit && !options[:format_yard]
|
876
|
+
if column.limit.is_a? Array
|
877
|
+
attrs << "(#{column.limit.join(', ')})"
|
878
|
+
else
|
879
|
+
column_type << "(#{column.limit})" unless hide_limit?(column_type, options)
|
880
|
+
end
|
881
|
+
end
|
882
|
+
end
|
883
|
+
|
884
|
+
# Check out if we got an array column
|
885
|
+
attrs << 'is an Array' if column.respond_to?(:array) && column.array
|
886
|
+
|
887
|
+
# Check out if we got a geometric column
|
888
|
+
# and print the type and SRID
|
889
|
+
if column.respond_to?(:geometry_type)
|
890
|
+
attrs << [column.geometry_type, column.try(:srid)].compact.join(', ')
|
891
|
+
elsif column.respond_to?(:geometric_type) && column.geometric_type.present?
|
892
|
+
attrs << [column.geometric_type.to_s.downcase, column.try(:srid)].compact.join(', ')
|
893
|
+
end
|
894
|
+
|
895
|
+
# Check if the column has indices and print "indexed" if true
|
896
|
+
# If the index includes another column, print it too.
|
897
|
+
if options[:simple_indexes] && klass.table_exists?# Check out if this column is indexed
|
898
|
+
indices = retrieve_indexes_from_table(klass)
|
899
|
+
if indices = indices.select { |ind| ind.columns.include? column.name }
|
900
|
+
indices.sort_by(&:name).each do |ind|
|
901
|
+
next if ind.columns.is_a?(String)
|
902
|
+
ind = ind.columns.reject! { |i| i == column.name }
|
903
|
+
attrs << (ind.empty? ? "indexed" : "indexed => [#{ind.join(", ")}]")
|
904
|
+
end
|
905
|
+
end
|
906
|
+
end
|
907
|
+
|
908
|
+
attrs
|
914
909
|
end
|
915
910
|
end
|
916
911
|
|
@@ -0,0 +1,113 @@
|
|
1
|
+
require_relative './helpers'
|
2
|
+
|
3
|
+
module AnnotateRoutes
|
4
|
+
class HeaderGenerator
|
5
|
+
PREFIX = '== Route Map'.freeze
|
6
|
+
PREFIX_MD = '## Route Map'.freeze
|
7
|
+
HEADER_ROW = ['Prefix', 'Verb', 'URI Pattern', 'Controller#Action'].freeze
|
8
|
+
|
9
|
+
class << self
|
10
|
+
def generate(options = {})
|
11
|
+
new(options, routes_map(options)).generate
|
12
|
+
end
|
13
|
+
|
14
|
+
private :new
|
15
|
+
|
16
|
+
private
|
17
|
+
|
18
|
+
def routes_map(options)
|
19
|
+
result = `rake routes`.chomp("\n").split(/\n/, -1)
|
20
|
+
|
21
|
+
# In old versions of Rake, the first line of output was the cwd. Not so
|
22
|
+
# much in newer ones. We ditch that line if it exists, and if not, we
|
23
|
+
# keep the line around.
|
24
|
+
result.shift if result.first =~ %r{^\(in \/}
|
25
|
+
|
26
|
+
ignore_routes = options[:ignore_routes]
|
27
|
+
regexp_for_ignoring_routes = ignore_routes ? /#{ignore_routes}/ : nil
|
28
|
+
|
29
|
+
# Skip routes which match given regex
|
30
|
+
# Note: it matches the complete line (route_name, path, controller/action)
|
31
|
+
if regexp_for_ignoring_routes
|
32
|
+
result.reject { |line| line =~ regexp_for_ignoring_routes }
|
33
|
+
else
|
34
|
+
result
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
def initialize(options, routes_map)
|
40
|
+
@options = options
|
41
|
+
@routes_map = routes_map
|
42
|
+
end
|
43
|
+
|
44
|
+
def generate
|
45
|
+
magic_comments_map, contents_without_magic_comments = Helpers.extract_magic_comments_from_array(routes_map)
|
46
|
+
|
47
|
+
out = []
|
48
|
+
|
49
|
+
magic_comments_map.each do |magic_comment|
|
50
|
+
out << magic_comment
|
51
|
+
end
|
52
|
+
out << '' if magic_comments_map.any?
|
53
|
+
|
54
|
+
out << comment(options[:wrapper_open]) if options[:wrapper_open]
|
55
|
+
|
56
|
+
out << comment(markdown? ? PREFIX_MD : PREFIX) + timestamp_if_required
|
57
|
+
out << comment
|
58
|
+
return out if contents_without_magic_comments.size.zero?
|
59
|
+
|
60
|
+
maxs = [HEADER_ROW.map(&:size)] + contents_without_magic_comments[1..-1].map { |line| line.split.map(&:size) }
|
61
|
+
|
62
|
+
if markdown?
|
63
|
+
max = maxs.map(&:max).compact.max
|
64
|
+
|
65
|
+
out << comment(content(HEADER_ROW, maxs))
|
66
|
+
out << comment(content(['-' * max, '-' * max, '-' * max, '-' * max], maxs))
|
67
|
+
else
|
68
|
+
out << comment(content(contents_without_magic_comments[0], maxs))
|
69
|
+
end
|
70
|
+
|
71
|
+
out += contents_without_magic_comments[1..-1].map { |line| comment(content(markdown? ? line.split(' ') : line, maxs)) }
|
72
|
+
out << comment(options[:wrapper_close]) if options[:wrapper_close]
|
73
|
+
|
74
|
+
out
|
75
|
+
end
|
76
|
+
|
77
|
+
private
|
78
|
+
|
79
|
+
attr_reader :options, :routes_map
|
80
|
+
|
81
|
+
def comment(row = '')
|
82
|
+
if row == ''
|
83
|
+
'#'
|
84
|
+
else
|
85
|
+
"# #{row}"
|
86
|
+
end
|
87
|
+
end
|
88
|
+
|
89
|
+
def content(line, maxs)
|
90
|
+
return line.rstrip unless markdown?
|
91
|
+
|
92
|
+
line.each_with_index.map { |elem, index| format_line_element(elem, maxs, index) }.join(' | ')
|
93
|
+
end
|
94
|
+
|
95
|
+
def format_line_element(elem, maxs, index)
|
96
|
+
min_length = maxs.map { |arr| arr[index] }.max || 0
|
97
|
+
format("%-#{min_length}.#{min_length}s", elem.tr('|', '-'))
|
98
|
+
end
|
99
|
+
|
100
|
+
def markdown?
|
101
|
+
options[:format_markdown]
|
102
|
+
end
|
103
|
+
|
104
|
+
def timestamp_if_required(time = Time.now)
|
105
|
+
if options[:timestamp]
|
106
|
+
time_formatted = time.strftime('%Y-%m-%d %H:%M')
|
107
|
+
" (Updated #{time_formatted})"
|
108
|
+
else
|
109
|
+
''
|
110
|
+
end
|
111
|
+
end
|
112
|
+
end
|
113
|
+
end
|