annotate 2.7.5 → 3.2.0

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