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.
@@ -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