annotaterb 4.0.0 → 4.1.1

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.
Files changed (29) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +4 -5
  3. data/VERSION +1 -1
  4. data/lib/annotate_rb/model_annotator/annotation_decider.rb +62 -0
  5. data/lib/annotate_rb/model_annotator/annotation_diff.rb +19 -0
  6. data/lib/annotate_rb/model_annotator/annotation_diff_generator.rb +44 -0
  7. data/lib/annotate_rb/model_annotator/annotation_generator.rb +94 -0
  8. data/lib/annotate_rb/model_annotator/annotator.rb +27 -31
  9. data/lib/annotate_rb/model_annotator/column_annotation_builder.rb +92 -0
  10. data/lib/annotate_rb/model_annotator/column_attributes_builder.rb +102 -0
  11. data/lib/annotate_rb/model_annotator/column_type_builder.rb +51 -0
  12. data/lib/annotate_rb/model_annotator/column_wrapper.rb +84 -0
  13. data/lib/annotate_rb/model_annotator/constants.rb +2 -2
  14. data/lib/annotate_rb/model_annotator/file_annotator.rb +9 -11
  15. data/lib/annotate_rb/model_annotator/file_annotator_instruction.rb +17 -0
  16. data/lib/annotate_rb/model_annotator/foreign_key_annotation_builder.rb +55 -0
  17. data/lib/annotate_rb/model_annotator/helper.rb +75 -22
  18. data/lib/annotate_rb/model_annotator/index_annotation_builder.rb +74 -0
  19. data/lib/annotate_rb/model_annotator/model_file_annotator.rb +21 -84
  20. data/lib/annotate_rb/model_annotator/model_files_getter.rb +4 -2
  21. data/lib/annotate_rb/model_annotator/model_wrapper.rb +155 -0
  22. data/lib/annotate_rb/model_annotator/related_files_list_builder.rb +137 -0
  23. data/lib/annotate_rb/model_annotator.rb +13 -1
  24. data/lib/annotate_rb/options.rb +16 -9
  25. data/lib/annotate_rb/parser.rb +0 -14
  26. data/lib/annotate_rb.rb +0 -1
  27. metadata +15 -4
  28. data/lib/annotate_rb/env.rb +0 -30
  29. data/lib/annotate_rb/model_annotator/schema_info.rb +0 -480
@@ -1,8 +1,6 @@
1
1
  module AnnotateRb
2
2
  module ModelAnnotator
3
3
  module Constants
4
- TRUE_RE = /^(true|t|yes|y|1)$/i.freeze
5
-
6
4
  ##
7
5
  # The set of available options to customize the behavior of Annotate.
8
6
  #
@@ -17,6 +15,8 @@ module AnnotateRb
17
15
  ALL_ANNOTATE_OPTIONS = ::AnnotateRb::Options::ALL_OPTION_KEYS
18
16
 
19
17
  SKIP_ANNOTATION_PREFIX = '# -\*- SkipSchemaAnnotations'.freeze
18
+
19
+ 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
20
20
  end
21
21
  end
22
22
  end
@@ -4,6 +4,10 @@ module AnnotateRb
4
4
  module ModelAnnotator
5
5
  class FileAnnotator
6
6
  class << self
7
+ def call_with_instructions(instruction)
8
+ call(instruction.file, instruction.annotation, instruction.position, instruction.options)
9
+ end
10
+
7
11
  # Add a schema block to a file. If the file already contains
8
12
  # a schema info block (a comment starting with "== Schema Information"),
9
13
  # check if it matches the block that is already there. If so, leave it be.
@@ -20,20 +24,14 @@ module AnnotateRb
20
24
  def call(file_name, info_block, position, options = {})
21
25
  return false unless File.exist?(file_name)
22
26
  old_content = File.read(file_name)
23
- return false if old_content =~ /#{Constants::SKIP_ANNOTATION_PREFIX}.*\n/
24
27
 
25
- # Ignore the Schema version line because it changes with each migration
26
- header_pattern = /(^# Table name:.*?\n(#.*[\r]?\n)*[\r]?)/
27
- old_header = old_content.match(header_pattern).to_s
28
- new_header = info_block.match(header_pattern).to_s
28
+ return false if old_content =~ /#{Constants::SKIP_ANNOTATION_PREFIX}.*\n/
29
29
 
30
- column_pattern = /^#[\t ]+[\w\*\.`]+[\t ]+.+$/
31
- old_columns = old_header && old_header.scan(column_pattern).sort
32
- new_columns = new_header && new_header.scan(column_pattern).sort
30
+ diff = AnnotationDiffGenerator.new(old_content, info_block).generate
33
31
 
34
- return false if old_columns == new_columns && !options[:force]
32
+ return false if !diff.changed? && !options[:force]
35
33
 
36
- abort "annotate error. #{file_name} needs to be updated, but annotate was run with `--frozen`." if options[:frozen]
34
+ abort "AnnotateRb error. #{file_name} needs to be updated, but annotaterb was run with `--frozen`." if options[:frozen]
37
35
 
38
36
  # Replace inline the old schema info with the new schema info
39
37
  wrapper_open = options[:wrapper_open] ? "# #{options[:wrapper_open]}\n" : ""
@@ -47,7 +45,7 @@ module AnnotateRb
47
45
  # need to insert it in correct position
48
46
  if old_annotation.empty? || options[:force]
49
47
  magic_comments_block = Helper.magic_comments_as_string(old_content)
50
- old_content.gsub!(Annotator::MAGIC_COMMENT_MATCHER, '')
48
+ old_content.gsub!(Constants::MAGIC_COMMENT_MATCHER, '')
51
49
 
52
50
  annotation_pattern = AnnotationPatternGenerator.call(options)
53
51
  old_content.sub!(annotation_pattern, '')
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ module AnnotateRb
4
+ module ModelAnnotator
5
+ # A plain old Ruby object (PORO) that contains all necessary information for FileAnnotator
6
+ class FileAnnotatorInstruction
7
+ def initialize(file, annotation, position, options = {})
8
+ @file = file # Path to file
9
+ @annotation = annotation # Annotation string
10
+ @position = position # Position in the file where to write the annotation to
11
+ @options = options
12
+ end
13
+
14
+ attr_reader :file, :annotation, :position, :options
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,55 @@
1
+ # frozen_string_literal: true
2
+
3
+ module AnnotateRb
4
+ module ModelAnnotator
5
+ class ForeignKeyAnnotationBuilder
6
+ def initialize(model, options)
7
+ @model = model
8
+ @options = options
9
+ end
10
+
11
+ def build
12
+ fk_info = if @options[:format_markdown]
13
+ "#\n# ### Foreign Keys\n#\n"
14
+ else
15
+ "#\n# Foreign Keys\n#\n"
16
+ end
17
+
18
+ return '' unless @model.connection.respond_to?(:supports_foreign_keys?) &&
19
+ @model.connection.supports_foreign_keys? && @model.connection.respond_to?(:foreign_keys)
20
+
21
+ foreign_keys = @model.connection.foreign_keys(@model.table_name)
22
+ return '' if foreign_keys.empty?
23
+
24
+ format_name = lambda do |fk|
25
+ return fk.options[:column] if fk.name.blank?
26
+
27
+ @options[:show_complete_foreign_keys] ? fk.name : fk.name.gsub(/(?<=^fk_rails_)[0-9a-f]{10}$/, '...')
28
+ end
29
+
30
+ max_size = foreign_keys.map(&format_name).map(&:size).max + 1
31
+ foreign_keys.sort_by { |fk| [format_name.call(fk), fk.column] }.each do |fk|
32
+ ref_info = "#{fk.column} => #{fk.to_table}.#{fk.primary_key}"
33
+ constraints_info = ''
34
+ constraints_info += "ON DELETE => #{fk.on_delete} " if fk.on_delete
35
+ constraints_info += "ON UPDATE => #{fk.on_update} " if fk.on_update
36
+ constraints_info = constraints_info.strip
37
+
38
+ fk_info += if @options[:format_markdown]
39
+ format("# * `%s`%s:\n# * **`%s`**\n",
40
+ format_name.call(fk),
41
+ constraints_info.blank? ? '' : " (_#{constraints_info}_)",
42
+ ref_info)
43
+ else
44
+ format("# %-#{max_size}.#{max_size}s %s %s",
45
+ format_name.call(fk),
46
+ "(#{ref_info})",
47
+ constraints_info).rstrip + "\n"
48
+ end
49
+ end
50
+
51
+ fk_info
52
+ end
53
+ end
54
+ end
55
+ end
@@ -3,51 +3,104 @@
3
3
  module AnnotateRb
4
4
  module ModelAnnotator
5
5
  module Helper
6
- MATCHED_TYPES = %w(test fixture factory serializer scaffold controller helper).freeze
6
+ INDEX_CLAUSES = {
7
+ unique: {
8
+ default: 'UNIQUE',
9
+ markdown: '_unique_'
10
+ },
11
+ where: {
12
+ default: 'WHERE',
13
+ markdown: '_where_'
14
+ },
15
+ using: {
16
+ default: 'USING',
17
+ markdown: '_using_'
18
+ }
19
+ }.freeze
7
20
 
8
21
  class << self
9
- def matched_types(options)
10
- types = MATCHED_TYPES.dup
11
- types << 'admin' if options[:active_admin] =~ Constants::TRUE_RE && !types.include?('admin')
12
- types << 'additional_file_patterns' if options[:additional_file_patterns].present?
22
+ def mb_chars_ljust(string, length)
23
+ string = string.to_s
24
+ padding = length - Helper.width(string)
25
+ if padding.positive?
26
+ string + (' ' * padding)
27
+ else
28
+ string[0..(length - 1)]
29
+ end
30
+ end
13
31
 
14
- types
32
+ def index_unique_info(index, format = :default)
33
+ index.unique ? " #{INDEX_CLAUSES[:unique][format]}" : ''
15
34
  end
16
35
 
17
- def magic_comments_as_string(content)
18
- magic_comments = content.scan(Annotator::MAGIC_COMMENT_MATCHER).flatten.compact
36
+ def index_where_info(index, format = :default)
37
+ value = index.try(:where).try(:to_s)
38
+ if value.blank?
39
+ ''
40
+ else
41
+ " #{INDEX_CLAUSES[:where][format]} #{value}"
42
+ end
43
+ end
19
44
 
20
- if magic_comments.any?
21
- magic_comments.join
45
+ def index_using_info(index, format = :default)
46
+ value = index.try(:using) && index.using.try(:to_sym)
47
+ if !value.blank? && value != :btree
48
+ " #{INDEX_CLAUSES[:using][format]} #{value}"
22
49
  else
23
50
  ''
24
51
  end
25
52
  end
26
53
 
27
- def skip_on_migration?
28
- Env.read('ANNOTATE_SKIP_ON_DB_MIGRATE') =~ Constants::TRUE_RE || Env.read('skip_on_db_migrate') =~ Constants::TRUE_RE
54
+ def map_col_type_to_ruby_classes(col_type)
55
+ case col_type
56
+ when 'integer' then Integer.to_s
57
+ when 'float' then Float.to_s
58
+ when 'decimal' then BigDecimal.to_s
59
+ when 'datetime', 'timestamp', 'time' then Time.to_s
60
+ when 'date' then Date.to_s
61
+ when 'text', 'string', 'binary', 'inet', 'uuid' then String.to_s
62
+ when 'json', 'jsonb' then Hash.to_s
63
+ when 'boolean' then 'Boolean'
64
+ end
65
+ end
66
+
67
+ def non_ascii_length(string)
68
+ string.to_s.chars.reject(&:ascii_only?).length
29
69
  end
30
70
 
31
- def include_routes?
32
- Env.read('routes') =~ Constants::TRUE_RE
71
+ # Simple quoting for the default column value
72
+ def quote(value)
73
+ case value
74
+ when NilClass then 'NULL'
75
+ when TrueClass then 'TRUE'
76
+ when FalseClass then 'FALSE'
77
+ when Float, Integer then value.to_s
78
+ # BigDecimals need to be output in a non-normalized form and quoted.
79
+ when BigDecimal then value.to_s('F')
80
+ when Array then value.map { |v| quote(v) }
81
+ else
82
+ value.inspect
83
+ end
33
84
  end
34
85
 
35
- def include_models?
36
- Env.read('models') =~ Constants::TRUE_RE
86
+ def width(string)
87
+ string.chars.inject(0) { |acc, elem| acc + (elem.bytesize == 3 ? 2 : 1) }
37
88
  end
38
89
 
39
- def true?(val)
40
- val.present? && Constants::TRUE_RE.match?(val)
90
+ def magic_comments_as_string(content)
91
+ magic_comments = content.scan(Constants::MAGIC_COMMENT_MATCHER).flatten.compact
92
+
93
+ if magic_comments.any?
94
+ magic_comments.join
95
+ else
96
+ ''
97
+ end
41
98
  end
42
99
 
43
100
  # TODO: Find another implementation that doesn't depend on ActiveSupport
44
101
  def fallback(*args)
45
102
  args.compact.detect(&:present?)
46
103
  end
47
-
48
- def reset_options(options)
49
- options.flatten.each { |key| Env.write(key, nil) }
50
- end
51
104
  end
52
105
  end
53
106
  end
@@ -0,0 +1,74 @@
1
+ # frozen_string_literal: true
2
+
3
+ module AnnotateRb
4
+ module ModelAnnotator
5
+ class IndexAnnotationBuilder
6
+ def initialize(model, options)
7
+ @model = model
8
+ @options = options
9
+ end
10
+
11
+ def build
12
+ index_info = if @options[:format_markdown]
13
+ "#\n# ### Indexes\n#\n"
14
+ else
15
+ "#\n# Indexes\n#\n"
16
+ end
17
+
18
+ indexes = @model.retrieve_indexes_from_table
19
+ return '' if indexes.empty?
20
+
21
+ max_size = indexes.collect { |index| index.name.size }.max + 1
22
+ indexes.sort_by(&:name).each do |index|
23
+ index_info += if @options[:format_markdown]
24
+ final_index_string_in_markdown(index)
25
+ else
26
+ final_index_string(index, max_size)
27
+ end
28
+ end
29
+
30
+ index_info
31
+ end
32
+
33
+ private
34
+
35
+ def final_index_string_in_markdown(index)
36
+ details = format(
37
+ '%s%s%s',
38
+ Helper.index_unique_info(index, :markdown),
39
+ Helper.index_where_info(index, :markdown),
40
+ Helper.index_using_info(index, :markdown)
41
+ ).strip
42
+ details = " (#{details})" unless details.blank?
43
+
44
+ format(
45
+ "# * `%s`%s:\n# * **`%s`**\n",
46
+ index.name,
47
+ details,
48
+ index_columns_info(index).join("`**\n# * **`")
49
+ )
50
+ end
51
+
52
+ def final_index_string(index, max_size)
53
+ format(
54
+ "# %-#{max_size}.#{max_size}s %s%s%s%s",
55
+ index.name,
56
+ "(#{index_columns_info(index).join(',')})",
57
+ Helper.index_unique_info(index),
58
+ Helper.index_where_info(index),
59
+ Helper.index_using_info(index)
60
+ ).rstrip + "\n"
61
+ end
62
+
63
+ def index_columns_info(index)
64
+ Array(index.columns).map do |col|
65
+ if index.try(:orders) && index.orders[col.to_s]
66
+ "#{col} #{index.orders[col.to_s].upcase}"
67
+ else
68
+ col.to_s.gsub("\r", '\r').gsub("\n", '\n')
69
+ end
70
+ end
71
+ end
72
+ end
73
+ end
74
+ end
@@ -2,36 +2,21 @@
2
2
 
3
3
  module AnnotateRb
4
4
  module ModelAnnotator
5
- # Not sure yet what the difference is between this and FileAnnotator
5
+ # Annotates a model file and its related files (controllers, factories, etc)
6
6
  class ModelFileAnnotator
7
7
  class << self
8
- def call(annotated, file, header, options)
8
+ def call(annotated, file, options)
9
9
  begin
10
- return false if /#{Constants::SKIP_ANNOTATION_PREFIX}.*/ =~ (File.exist?(file) ? File.read(file) : '')
11
10
  klass = ModelClassGetter.call(file, options)
12
11
 
13
- klass_is_a_class = klass.is_a?(Class)
14
- klass_inherits_active_record_base = klass < ActiveRecord::Base
15
- klass_is_not_abstract = !klass.abstract_class?
16
- klass_table_exists = klass.table_exists?
17
-
18
- not_sure_this_conditional = (!options[:exclude_sti_subclasses] || !(klass.superclass < ActiveRecord::Base && klass.table_name == klass.superclass.table_name))
19
-
20
- annotate_conditions = [
21
- klass_is_a_class,
22
- klass_inherits_active_record_base,
23
- not_sure_this_conditional,
24
- klass_is_not_abstract,
25
- klass_table_exists
26
- ]
27
-
28
- do_annotate = annotate_conditions.all?
29
-
30
- if do_annotate
31
- files_annotated = annotate(klass, file, header, options)
32
- annotated.concat(files_annotated)
12
+ instructions = build_instructions(klass, file, options)
13
+ instructions.each do |instruction|
14
+ if FileAnnotator.call_with_instructions(instruction)
15
+ annotated << instruction.file
16
+ end
33
17
  end
34
18
 
19
+ annotated
35
20
  rescue BadModelFileError => e
36
21
  unless options[:ignore_unknown_models]
37
22
  $stderr.puts "Unable to annotate #{file}: #{e.message}"
@@ -45,72 +30,24 @@ module AnnotateRb
45
30
 
46
31
  private
47
32
 
48
- # Given the name of an ActiveRecord class, create a schema
49
- # info block (basically a comment containing information
50
- # on the columns and their types) and put it at the front
51
- # of the model and fixture source files.
52
- #
53
- # === Options (opts)
54
- # :position_in_class<Symbol>:: where to place the annotated section in model file
55
- # :position_in_test<Symbol>:: where to place the annotated section in test/spec file(s)
56
- # :position_in_fixture<Symbol>:: where to place the annotated section in fixture file
57
- # :position_in_factory<Symbol>:: where to place the annotated section in factory file
58
- # :position_in_serializer<Symbol>:: where to place the annotated section in serializer file
59
- # :exclude_tests<Symbol>:: whether to skip modification of test/spec files
60
- # :exclude_fixtures<Symbol>:: whether to skip modification of fixture files
61
- # :exclude_factories<Symbol>:: whether to skip modification of factory files
62
- # :exclude_serializers<Symbol>:: whether to skip modification of serializer files
63
- # :exclude_scaffolds<Symbol>:: whether to skip modification of scaffold files
64
- # :exclude_controllers<Symbol>:: whether to skip modification of controller files
65
- # :exclude_helpers<Symbol>:: whether to skip modification of helper files
66
- # :exclude_sti_subclasses<Symbol>:: whether to skip modification of files for STI subclasses
67
- #
68
- # == Returns:
69
- # an array of file names that were annotated.
70
- #
71
- def annotate(klass, file, header, options = {})
72
- begin
73
- klass.reset_column_information
74
- info = SchemaInfo.generate(klass, header, options)
75
- model_name = klass.name.underscore
76
- table_name = klass.table_name
77
- model_file_name = File.join(file)
78
- annotated = []
79
-
80
- if FileAnnotator.call(model_file_name, info, :position_in_class, options)
81
- annotated << model_file_name
82
- end
83
-
84
- Helper.matched_types(options).each do |key|
85
- exclusion_key = "exclude_#{key.pluralize}".to_sym
86
- position_key = "position_in_#{key}".to_sym
87
-
88
- # Same options for active_admin models
89
- if key == 'admin'
90
- exclusion_key = 'exclude_class'.to_sym
91
- position_key = 'position_in_class'.to_sym
92
- end
33
+ def build_instructions(klass, file, options = {})
34
+ instructions = []
93
35
 
94
- next if options[exclusion_key]
36
+ klass.reset_column_information
37
+ annotation = AnnotationGenerator.new(klass, options).generate
38
+ model_name = klass.name.underscore
39
+ table_name = klass.table_name
95
40
 
96
- patterns = PatternGetter.call(options, key)
41
+ model_instruction = FileAnnotatorInstruction.new(file, annotation, :position_in_class, options)
42
+ instructions << model_instruction
97
43
 
98
- patterns
99
- .map { |f| FileNameResolver.call(f, model_name, table_name) }
100
- .map { |f| Dir.glob(f) }
101
- .flatten
102
- .each do |f|
103
- if FileAnnotator.call(f, info, position_key, options)
104
- annotated << f
105
- end
106
- end
107
- end
108
- rescue StandardError => e
109
- $stderr.puts "Unable to annotate #{file}: #{e.message}"
110
- $stderr.puts "\t" + e.backtrace.join("\n\t") if options[:trace]
44
+ related_files = RelatedFilesListBuilder.new(file, model_name, table_name, options).build
45
+ related_file_instructions = related_files.map do |f, position_key|
46
+ _instruction = FileAnnotatorInstruction.new(f, annotation, position_key, options)
111
47
  end
48
+ instructions.concat(related_file_instructions)
112
49
 
113
- annotated
50
+ instructions
114
51
  end
115
52
  end
116
53
  end
@@ -11,6 +11,8 @@ module AnnotateRb
11
11
  def call(options)
12
12
  model_files = []
13
13
 
14
+ # Note: This is currently broken as we don't set `is_rake` anywhere.
15
+ # It's an artifact from the old Annotate gem and how it did control flow.
14
16
  model_files = list_model_files_from_argument(options) if !options[:is_rake]
15
17
 
16
18
  return model_files if !model_files.empty?
@@ -30,7 +32,7 @@ module AnnotateRb
30
32
  rescue SystemCallError
31
33
  $stderr.puts "No models found in directory '#{options[:model_dir].join("', '")}'."
32
34
  $stderr.puts "Either specify models on the command line, or use the --model-dir option."
33
- $stderr.puts "Call 'annotate --help' for more info."
35
+ $stderr.puts "Call 'annotaterb --help' for more info."
34
36
  # exit 1 # TODO: Return exit code back to caller. Right now it messes up RSpec being able to run
35
37
  end
36
38
 
@@ -50,7 +52,7 @@ module AnnotateRb
50
52
 
51
53
  if model_files.size != specified_files.size
52
54
  $stderr.puts "The specified file could not be found in directory '#{options[:model_dir].join("', '")}'."
53
- $stderr.puts "Call 'annotate --help' for more info."
55
+ $stderr.puts "Call 'annotaterb --help' for more info."
54
56
  # exit 1 # TODO: Return exit code back to caller. Right now it messes up RSpec being able to run
55
57
  end
56
58
 
@@ -0,0 +1,155 @@
1
+ # frozen_string_literal: true
2
+
3
+ module AnnotateRb
4
+ module ModelAnnotator
5
+ class ModelWrapper
6
+ # Should be the wrapper for an ActiveRecord model that serves as the source of truth of the model
7
+ # of the model that we're annotating
8
+
9
+ def initialize(klass, options = {})
10
+ @klass = klass
11
+ @options = options
12
+ end
13
+
14
+ # Gets the columns of the ActiveRecord model, processes them, and then returns them.
15
+ def columns
16
+ @columns ||= begin
17
+ cols = raw_columns
18
+ cols += translated_columns
19
+
20
+ ignore_columns = @options[:ignore_columns]
21
+ if ignore_columns
22
+ cols = cols.reject do |col|
23
+ col.name.match(/#{ignore_columns}/)
24
+ end
25
+ end
26
+
27
+ cols = cols.sort_by(&:name) if @options[:sort]
28
+ cols = classified_sort(cols) if @options[:classified_sort]
29
+
30
+ cols
31
+ end
32
+ end
33
+
34
+ def connection
35
+ @klass.connection
36
+ end
37
+
38
+ # Returns the unmodified model columns
39
+ def raw_columns
40
+ @raw_columns ||= @klass.columns
41
+ end
42
+
43
+ def primary_key
44
+ @klass.primary_key
45
+ end
46
+
47
+ def table_exists?
48
+ @klass.table_exists?
49
+ end
50
+
51
+ def column_defaults
52
+ @klass.column_defaults
53
+ end
54
+
55
+ # Add columns managed by the globalize gem if this gem is being used.
56
+ # TODO: Audit if this is still needed, it seems like Globalize gem is no longer maintained
57
+ def translated_columns
58
+ return [] unless @klass.respond_to?(:translation_class)
59
+
60
+ ignored_cols = ignored_translation_table_columns
61
+
62
+ @klass.translation_class.columns.reject do |col|
63
+ ignored_cols.include? col.name.to_sym
64
+ end
65
+ end
66
+
67
+ def table_name
68
+ @klass.table_name
69
+ end
70
+
71
+ def model_name
72
+ @klass.name.underscore
73
+ end
74
+
75
+ # Calculates the max width of the schema for the model by looking at the columns, schema comments, with respect
76
+ # to the options.
77
+ def max_schema_info_width
78
+ cols = columns
79
+
80
+ if with_comments?
81
+ max_size = cols.map do |column|
82
+ column.name.size + (column.comment ? Helper.width(column.comment) : 0)
83
+ end.max || 0
84
+ max_size += 2
85
+ else
86
+ max_size = cols.map(&:name).map(&:size).max
87
+ end
88
+ max_size += @options[:format_rdoc] ? 5 : 1
89
+
90
+ max_size
91
+ end
92
+
93
+ def retrieve_indexes_from_table
94
+ table_name = @klass.table_name
95
+ return [] unless table_name
96
+
97
+ indexes = @klass.connection.indexes(table_name)
98
+ return indexes if indexes.any? || !@klass.table_name_prefix
99
+
100
+ # Try to search the table without prefix
101
+ table_name_without_prefix = table_name.to_s.sub(@klass.table_name_prefix, '')
102
+ @klass.connection.indexes(table_name_without_prefix)
103
+ end
104
+
105
+ def with_comments?
106
+ @options[:with_comment] &&
107
+ raw_columns.first.respond_to?(:comment) &&
108
+ raw_columns.map(&:comment).any? { |comment| !comment.nil? }
109
+ end
110
+
111
+ def classified_sort(cols)
112
+ rest_cols = []
113
+ timestamps = []
114
+ associations = []
115
+ id = nil
116
+
117
+ cols.each do |c|
118
+ if c.name.eql?('id')
119
+ id = c
120
+ elsif c.name.eql?('created_at') || c.name.eql?('updated_at')
121
+ timestamps << c
122
+ elsif c.name[-3, 3].eql?('_id')
123
+ associations << c
124
+ else
125
+ rest_cols << c
126
+ end
127
+ end
128
+ [rest_cols, timestamps, associations].each { |a| a.sort_by!(&:name) }
129
+
130
+ ([id] << rest_cols << timestamps << associations).flatten.compact
131
+ end
132
+
133
+ # These are the columns that the globalize gem needs to work but
134
+ # are not necessary for the models to be displayed as annotations.
135
+ def ignored_translation_table_columns
136
+ # Construct the foreign column name in the translations table
137
+ # eg. Model: Car, foreign column name: car_id
138
+ foreign_column_name = [
139
+ @klass.translation_class.to_s
140
+ .gsub('::Translation', '').gsub('::', '_')
141
+ .downcase,
142
+ '_id'
143
+ ].join.to_sym
144
+
145
+ [
146
+ :id,
147
+ :created_at,
148
+ :updated_at,
149
+ :locale,
150
+ foreign_column_name
151
+ ]
152
+ end
153
+ end
154
+ end
155
+ end