annotate 3.0.2 → 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} +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
|