annotate 3.0.2 → 3.2.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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 test_files(root_directory)
112
- [
113
- File.join(root_directory, UNIT_TEST_DIR, "%MODEL_NAME%_test.rb"),
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
- 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
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 = files_by_pattern(root_directory, pattern_type, options)
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 = if ignore_columns = options[:ignore_columns]
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 = ->(fk) { options[:show_complete_foreign_keys] ? fk.name : fk.name.gsub(/(?<=^fk_rails_)[0-9a-f]{10}$/, '...') }
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\*`]+[\t ]+.+$/
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!(magic_comment_matcher, '')
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 + "\n" + old_content
403
+ magic_comments_block + wrapped_info_block + old_content.lstrip
544
404
  else
545
- magic_comments_block + "\n" + wrapped_info_block + "\n" + old_content
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(magic_comment_matcher).flatten.compact
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
- loaded_model_class = get_loaded_model_by_path(model_path)
747
- return loaded_model_class if loaded_model_class
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.select { |path| absolute_file.include?(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 = klass.columns.map do |column|
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 = klass.column_names.map(&:size).max
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 == 1 ? 1 : 2) }
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