annotate 3.0.2 → 3.2.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/{AUTHORS.rdoc → AUTHORS.md} +2 -2
- data/CHANGELOG.md +326 -0
- data/{README.rdoc → README.md} +139 -105
- data/RELEASE.md +19 -0
- data/annotate.gemspec +11 -28
- data/bin/annotate +3 -3
- data/lib/annotate/annotate_models/file_patterns.rb +127 -0
- data/lib/annotate/annotate_models.rb +156 -181
- 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 +44 -177
- data/lib/annotate/constants.rb +33 -0
- data/lib/annotate/helpers.rb +30 -0
- data/lib/annotate/parser.rb +127 -75
- data/lib/annotate/version.rb +1 -1
- data/lib/annotate.rb +21 -80
- data/lib/generators/annotate/templates/auto_annotate_models.rake +3 -1
- data/lib/tasks/annotate_models.rake +36 -35
- data/lib/tasks/annotate_models_migrate.rake +17 -4
- data/lib/tasks/annotate_routes.rake +12 -6
- data/potato.md +41 -0
- metadata +23 -18
- data/CHANGELOG.rdoc +0 -238
- data/TODO.rdoc +0 -11
@@ -3,6 +3,7 @@
|
|
3
3
|
require 'bigdecimal'
|
4
4
|
|
5
5
|
require 'annotate/constants'
|
6
|
+
require_relative 'annotate_models/file_patterns'
|
6
7
|
|
7
8
|
module AnnotateModels
|
8
9
|
# Annotate Models plugin use this header
|
@@ -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,82 +67,24 @@ 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
|
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
|
153
73
|
|
154
|
-
|
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
|
161
|
-
|
162
|
-
def files_by_pattern(root_directory, pattern_type, options)
|
163
|
-
case pattern_type
|
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 'additional_file_patterns'
|
170
|
-
[options[:additional_file_patterns] || []].flatten
|
171
|
-
when 'controller'
|
172
|
-
[File.join(root_directory, CONTROLLER_DIR, "%PLURALIZED_MODEL_NAME%_controller.rb")]
|
173
|
-
when 'admin'
|
174
|
-
[File.join(root_directory, ACTIVEADMIN_DIR, "%MODEL_NAME%.rb")]
|
175
|
-
when 'helper'
|
176
|
-
[File.join(root_directory, HELPER_DIR, "%PLURALIZED_MODEL_NAME%_helper.rb")]
|
74
|
+
if @skip_subdirectory_model_load.blank?
|
75
|
+
false
|
177
76
|
else
|
178
|
-
|
77
|
+
@skip_subdirectory_model_load
|
179
78
|
end
|
180
79
|
end
|
181
80
|
|
81
|
+
attr_writer :skip_subdirectory_model_load
|
82
|
+
|
182
83
|
def get_patterns(options, pattern_types = [])
|
183
84
|
current_patterns = []
|
184
85
|
root_dir.each do |root_directory|
|
185
86
|
Array(pattern_types).each do |pattern_type|
|
186
|
-
patterns =
|
87
|
+
patterns = FilePatterns.generate(root_directory, pattern_type, options)
|
187
88
|
|
188
89
|
current_patterns += if pattern_type.to_sym == :additional_file_patterns
|
189
90
|
patterns
|
@@ -244,68 +145,24 @@ module AnnotateModels
|
|
244
145
|
info << "# #{ '-' * ( max_size + md_names_overhead ) } | #{'-' * md_type_allowance} | #{ '-' * 27 }\n"
|
245
146
|
end
|
246
147
|
|
247
|
-
cols =
|
248
|
-
klass.columns.reject do |col|
|
249
|
-
col.name.match(/#{ignore_columns}/)
|
250
|
-
end
|
251
|
-
else
|
252
|
-
klass.columns
|
253
|
-
end
|
254
|
-
|
255
|
-
cols = cols.sort_by(&:name) if options[:sort]
|
256
|
-
cols = classified_sort(cols) if options[:classified_sort]
|
148
|
+
cols = columns(klass, options)
|
257
149
|
cols.each do |col|
|
258
150
|
col_type = get_col_type(col)
|
259
|
-
attrs =
|
260
|
-
attrs << "default(#{schema_default(klass, col)})" unless col.default.nil? || hide_default?(col_type, options)
|
261
|
-
attrs << 'unsigned' if col.respond_to?(:unsigned?) && col.unsigned?
|
262
|
-
attrs << 'not null' unless col.null
|
263
|
-
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)
|
264
|
-
|
265
|
-
if col_type == 'decimal'
|
266
|
-
col_type << "(#{col.precision}, #{col.scale})"
|
267
|
-
elsif col_type != 'spatial'
|
268
|
-
if col.limit
|
269
|
-
if col.limit.is_a? Array
|
270
|
-
attrs << "(#{col.limit.join(', ')})"
|
271
|
-
else
|
272
|
-
col_type << "(#{col.limit})" unless hide_limit?(col_type, options)
|
273
|
-
end
|
274
|
-
end
|
275
|
-
end
|
276
|
-
|
277
|
-
# Check out if we got an array column
|
278
|
-
attrs << 'is an Array' if col.respond_to?(:array) && col.array
|
279
|
-
|
280
|
-
# Check out if we got a geometric column
|
281
|
-
# and print the type and SRID
|
282
|
-
if col.respond_to?(:geometry_type)
|
283
|
-
attrs << "#{col.geometry_type}, #{col.srid}"
|
284
|
-
elsif col.respond_to?(:geometric_type) && col.geometric_type.present?
|
285
|
-
attrs << "#{col.geometric_type.to_s.downcase}, #{col.srid}"
|
286
|
-
end
|
287
|
-
|
288
|
-
# Check if the column has indices and print "indexed" if true
|
289
|
-
# If the index includes another column, print it too.
|
290
|
-
if options[:simple_indexes] && klass.table_exists?# Check out if this column is indexed
|
291
|
-
indices = retrieve_indexes_from_table(klass)
|
292
|
-
if indices = indices.select { |ind| ind.columns.include? col.name }
|
293
|
-
indices.sort_by(&:name).each do |ind|
|
294
|
-
next if ind.columns.is_a?(String)
|
295
|
-
ind = ind.columns.reject! { |i| i == col.name }
|
296
|
-
attrs << (ind.empty? ? "indexed" : "indexed => [#{ind.join(", ")}]")
|
297
|
-
end
|
298
|
-
end
|
299
|
-
end
|
151
|
+
attrs = get_attributes(col, col_type, klass, options)
|
300
152
|
col_name = if with_comments?(klass, options) && col.comment
|
301
|
-
"#{col.name}(#{col.comment})"
|
153
|
+
"#{col.name}(#{col.comment.gsub(/\n/, "\\n")})"
|
302
154
|
else
|
303
155
|
col.name
|
304
156
|
end
|
157
|
+
|
305
158
|
if options[:format_rdoc]
|
306
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"
|
307
164
|
elsif options[:format_markdown]
|
308
|
-
name_remainder = max_size - col_name.length
|
165
|
+
name_remainder = max_size - col_name.length - non_ascii_length(col_name)
|
309
166
|
type_remainder = (md_type_allowance - 2) - col_type.length
|
310
167
|
info << (sprintf("# **`%s`**%#{name_remainder}s | `%s`%#{type_remainder}s | `%s`", col_name, " ", col_type, " ", attrs.join(", ").rstrip)).gsub('``', ' ').rstrip + "\n"
|
311
168
|
else
|
@@ -472,7 +329,10 @@ module AnnotateModels
|
|
472
329
|
foreign_keys = klass.connection.foreign_keys(klass.table_name)
|
473
330
|
return '' if foreign_keys.empty?
|
474
331
|
|
475
|
-
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
|
476
336
|
|
477
337
|
max_size = foreign_keys.map(&format_name).map(&:size).max + 1
|
478
338
|
foreign_keys.sort_by {|fk| [format_name.call(fk), fk.column]}.each do |fk|
|
@@ -515,7 +375,7 @@ module AnnotateModels
|
|
515
375
|
old_header = old_content.match(header_pattern).to_s
|
516
376
|
new_header = info_block.match(header_pattern).to_s
|
517
377
|
|
518
|
-
column_pattern = /^#[\t ]+[\w
|
378
|
+
column_pattern = /^#[\t ]+[\w\*\.`]+[\t ]+.+$/
|
519
379
|
old_columns = old_header && old_header.scan(column_pattern).sort
|
520
380
|
new_columns = new_header && new_header.scan(column_pattern).sort
|
521
381
|
|
@@ -534,15 +394,15 @@ module AnnotateModels
|
|
534
394
|
# need to insert it in correct position
|
535
395
|
if old_annotation.empty? || options[:force]
|
536
396
|
magic_comments_block = magic_comments_as_string(old_content)
|
537
|
-
old_content.gsub!(
|
397
|
+
old_content.gsub!(MAGIC_COMMENT_MATCHER, '')
|
538
398
|
old_content.sub!(annotate_pattern(options), '')
|
539
399
|
|
540
400
|
new_content = if %w(after bottom).include?(options[position].to_s)
|
541
401
|
magic_comments_block + (old_content.rstrip + "\n\n" + wrapped_info_block)
|
542
402
|
elsif magic_comments_block.empty?
|
543
|
-
magic_comments_block + wrapped_info_block +
|
403
|
+
magic_comments_block + wrapped_info_block + old_content.lstrip
|
544
404
|
else
|
545
|
-
magic_comments_block + "\n" + wrapped_info_block +
|
405
|
+
magic_comments_block + "\n" + wrapped_info_block + old_content.lstrip
|
546
406
|
end
|
547
407
|
else
|
548
408
|
# replace the old annotation with the new one
|
@@ -558,12 +418,8 @@ module AnnotateModels
|
|
558
418
|
true
|
559
419
|
end
|
560
420
|
|
561
|
-
def magic_comment_matcher
|
562
|
-
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))/)
|
563
|
-
end
|
564
|
-
|
565
421
|
def magic_comments_as_string(content)
|
566
|
-
magic_comments = content.scan(
|
422
|
+
magic_comments = content.scan(MAGIC_COMMENT_MATCHER).flatten.compact
|
567
423
|
|
568
424
|
if magic_comments.any?
|
569
425
|
magic_comments.join
|
@@ -743,14 +599,17 @@ module AnnotateModels
|
|
743
599
|
|
744
600
|
# Retrieve loaded model class
|
745
601
|
def get_loaded_model(model_path, file)
|
746
|
-
|
747
|
-
|
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
|
748
606
|
|
749
607
|
# We cannot get loaded model when `model_path` is loaded by Rails
|
750
608
|
# auto_load/eager_load paths. Try all possible model paths one by one.
|
751
609
|
absolute_file = File.expand_path(file)
|
752
610
|
model_paths =
|
753
|
-
$LOAD_PATH.
|
611
|
+
$LOAD_PATH.map(&:to_s)
|
612
|
+
.select { |path| absolute_file.include?(path) }
|
754
613
|
.map { |path| absolute_file.sub(path, '').sub(/\.rb$/, '').sub(/^\//, '') }
|
755
614
|
model_paths
|
756
615
|
.map { |path| get_loaded_model_by_path(path) }
|
@@ -773,6 +632,7 @@ module AnnotateModels
|
|
773
632
|
def parse_options(options = {})
|
774
633
|
self.model_dir = split_model_dir(options[:model_dir]) if options[:model_dir]
|
775
634
|
self.root_dir = options[:root_dir] if options[:root_dir]
|
635
|
+
self.skip_subdirectory_model_load = options[:skip_subdirectory_model_load].present?
|
776
636
|
end
|
777
637
|
|
778
638
|
def split_model_dir(option_value)
|
@@ -813,7 +673,7 @@ module AnnotateModels
|
|
813
673
|
begin
|
814
674
|
return false if /#{SKIP_ANNOTATION_PREFIX}.*/ =~ (File.exist?(file) ? File.read(file) : '')
|
815
675
|
klass = get_model_class(file)
|
816
|
-
do_annotate = klass &&
|
676
|
+
do_annotate = klass.is_a?(Class) &&
|
817
677
|
klass < ActiveRecord::Base &&
|
818
678
|
(!options[:exclude_sti_subclasses] || !(klass.superclass < ActiveRecord::Base && klass.table_name == klass.superclass.table_name)) &&
|
819
679
|
!klass.abstract_class? &&
|
@@ -902,13 +762,15 @@ module AnnotateModels
|
|
902
762
|
end
|
903
763
|
|
904
764
|
def max_schema_info_width(klass, options)
|
765
|
+
cols = columns(klass, options)
|
766
|
+
|
905
767
|
if with_comments?(klass, options)
|
906
|
-
max_size =
|
768
|
+
max_size = cols.map do |column|
|
907
769
|
column.name.size + (column.comment ? width(column.comment) : 0)
|
908
770
|
end.max || 0
|
909
771
|
max_size += 2
|
910
772
|
else
|
911
|
-
max_size =
|
773
|
+
max_size = cols.map(&:name).map(&:size).max
|
912
774
|
end
|
913
775
|
max_size += options[:format_rdoc] ? 5 : 1
|
914
776
|
|
@@ -920,7 +782,7 @@ module AnnotateModels
|
|
920
782
|
end
|
921
783
|
|
922
784
|
def width(string)
|
923
|
-
string.chars.inject(0) { |acc, elem| acc + (elem.bytesize ==
|
785
|
+
string.chars.inject(0) { |acc, elem| acc + (elem.bytesize == 3 ? 2 : 1) }
|
924
786
|
end
|
925
787
|
|
926
788
|
def mb_chars_ljust(string, length)
|
@@ -932,6 +794,119 @@ module AnnotateModels
|
|
932
794
|
string[0..length-1]
|
933
795
|
end
|
934
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
|
909
|
+
end
|
935
910
|
end
|
936
911
|
|
937
912
|
class BadModelFileError < LoadError
|
@@ -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
|
@@ -0,0 +1,69 @@
|
|
1
|
+
module AnnotateRoutes
|
2
|
+
module Helpers
|
3
|
+
MAGIC_COMMENT_MATCHER = Regexp.new(/(^#\s*encoding:.*)|(^# coding:.*)|(^# -\*- coding:.*)|(^# -\*- encoding\s?:.*)|(^#\s*frozen_string_literal:.+)|(^# -\*- frozen_string_literal\s*:.+-\*-)/).freeze
|
4
|
+
|
5
|
+
class << self
|
6
|
+
# TODO: write the method doc using ruby rdoc formats
|
7
|
+
# This method returns an array of 'real_content' and 'header_position'.
|
8
|
+
# 'header_position' will either be :before, :after, or
|
9
|
+
# a number. If the number is > 0, the
|
10
|
+
# annotation was found somewhere in the
|
11
|
+
# middle of the file. If the number is
|
12
|
+
# zero, no annotation was found.
|
13
|
+
def strip_annotations(content)
|
14
|
+
real_content = []
|
15
|
+
mode = :content
|
16
|
+
header_position = 0
|
17
|
+
|
18
|
+
content.split(/\n/, -1).each_with_index do |line, line_number|
|
19
|
+
if mode == :header && line !~ /\s*#/
|
20
|
+
mode = :content
|
21
|
+
real_content << line unless line.blank?
|
22
|
+
elsif mode == :content
|
23
|
+
if line =~ /^\s*#\s*== Route.*$/
|
24
|
+
header_position = line_number + 1 # index start's at 0
|
25
|
+
mode = :header
|
26
|
+
else
|
27
|
+
real_content << line
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
real_content_and_header_position(real_content, header_position)
|
33
|
+
end
|
34
|
+
|
35
|
+
# @param [Array<String>] content
|
36
|
+
# @return [Array<String>] all found magic comments
|
37
|
+
# @return [Array<String>] content without magic comments
|
38
|
+
def extract_magic_comments_from_array(content_array)
|
39
|
+
magic_comments = []
|
40
|
+
new_content = []
|
41
|
+
|
42
|
+
content_array.each do |row|
|
43
|
+
if row =~ MAGIC_COMMENT_MATCHER
|
44
|
+
magic_comments << row.strip
|
45
|
+
else
|
46
|
+
new_content << row
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
[magic_comments, new_content]
|
51
|
+
end
|
52
|
+
|
53
|
+
private
|
54
|
+
|
55
|
+
def real_content_and_header_position(real_content, header_position)
|
56
|
+
# By default assume the annotation was found in the middle of the file
|
57
|
+
|
58
|
+
# ... unless we have evidence it was at the beginning ...
|
59
|
+
return real_content, :before if header_position == 1
|
60
|
+
|
61
|
+
# ... or that it was at the end.
|
62
|
+
return real_content, :after if header_position >= real_content.count
|
63
|
+
|
64
|
+
# and the default
|
65
|
+
return real_content, header_position
|
66
|
+
end
|
67
|
+
end
|
68
|
+
end
|
69
|
+
end
|