diff --git a/.rubocop_todo.yml b/.rubocop_todo.yml
index 246a23ea6..ab41236a9 100644
--- a/.rubocop_todo.yml
+++ b/.rubocop_todo.yml
@@ -1,6 +1,6 @@
# This configuration was generated by
# `rubocop --auto-gen-config`
-# on 2020-04-06 10:26:11 +0900 using RuboCop version 0.68.1.
+# on 2020-04-07 16:25:20 +0900 using RuboCop version 0.68.1.
# The point is for the user to remove these configuration records
# one by one as the offenses are removed from the code base.
# Note that changes in the inspected code, or installation of new
@@ -21,7 +21,7 @@ Gemspec/RequiredRubyVersion:
Exclude:
- 'annotate.gemspec'
-# Offense count: 65
+# Offense count: 62
# Cop supports --auto-correct.
# Configuration parameters: EnforcedHashRocketStyle, EnforcedColonStyle, EnforcedLastArgumentHashStyle.
# SupportedHashRocketStyles: key, separator, table
@@ -30,7 +30,7 @@ Gemspec/RequiredRubyVersion:
Layout/AlignHash:
Exclude:
- 'lib/generators/annotate/templates/auto_annotate_models.rake'
- - 'spec/lib/annotate/annotate_models_spec.rb'
+ - 'spec/spec_helper.rb'
# Offense count: 1
# Cop supports --auto-correct.
@@ -40,7 +40,7 @@ Layout/BlockAlignment:
Exclude:
- 'lib/annotate/annotate_models.rb'
-# Offense count: 9
+# Offense count: 7
# Cop supports --auto-correct.
Layout/EmptyLineAfterGuardClause:
Exclude:
@@ -55,13 +55,12 @@ Layout/EmptyLineAfterMagicComment:
- 'annotate.gemspec'
- 'spec/lib/annotate/annotate_models_spec.rb'
-# Offense count: 3
+# Offense count: 2
# Cop supports --auto-correct.
# Configuration parameters: AllowForAlignment, AllowBeforeTrailingComments, ForceEqualSignAlignment.
Layout/ExtraSpacing:
Exclude:
- 'Guardfile'
- - 'lib/annotate/annotate_models.rb'
- 'lib/tasks/annotate_routes.rake'
# Offense count: 16
@@ -71,7 +70,7 @@ Layout/ExtraSpacing:
Layout/IndentFirstArrayElement:
EnforcedStyle: consistent
-# Offense count: 5
+# Offense count: 4
# Cop supports --auto-correct.
# Configuration parameters: EnforcedStyle, IndentationWidth.
# SupportedStyles: aligned, indented
@@ -87,38 +86,13 @@ Layout/SpaceAroundEqualsInParameterDefault:
Exclude:
- 'lib/annotate/annotate_routes.rb'
-# Offense count: 4
+# Offense count: 2
# Cop supports --auto-correct.
# Configuration parameters: AllowForAlignment.
Layout/SpaceAroundOperators:
Exclude:
- - 'lib/annotate/annotate_models.rb'
- 'lib/tasks/annotate_routes.rake'
-# Offense count: 1
-# Cop supports --auto-correct.
-# Configuration parameters: EnforcedStyle, EnforcedStyleForEmptyBraces.
-# SupportedStyles: space, no_space
-# SupportedStylesForEmptyBraces: space, no_space
-Layout/SpaceBeforeBlockBraces:
- Exclude:
- - 'lib/annotate/annotate_models.rb'
-
-# Offense count: 1
-# Cop supports --auto-correct.
-Layout/SpaceBeforeComment:
- Exclude:
- - 'lib/annotate/annotate_models.rb'
-
-# Offense count: 4
-# Cop supports --auto-correct.
-# Configuration parameters: EnforcedStyle, EnforcedStyleForEmptyBraces, SpaceBeforeBlockParameters.
-# SupportedStyles: space, no_space
-# SupportedStylesForEmptyBraces: space, no_space
-Layout/SpaceInsideBlockBraces:
- Exclude:
- - 'lib/annotate/annotate_models.rb'
-
# Offense count: 4
# Cop supports --auto-correct.
# Configuration parameters: EnforcedStyle, EnforcedStyleForEmptyBraces.
@@ -128,22 +102,6 @@ Layout/SpaceInsideHashLiteralBraces:
Exclude:
- 'lib/tasks/annotate_models.rake'
-# Offense count: 4
-# Cop supports --auto-correct.
-# Configuration parameters: EnforcedStyle.
-# SupportedStyles: space, no_space
-Layout/SpaceInsideParens:
- Exclude:
- - 'lib/annotate/annotate_models.rb'
-
-# Offense count: 2
-# Cop supports --auto-correct.
-# Configuration parameters: EnforcedStyle.
-# SupportedStyles: space, no_space
-Layout/SpaceInsideStringInterpolation:
- Exclude:
- - 'lib/annotate/annotate_models.rb'
-
# Offense count: 2
# Cop supports --auto-correct.
# Configuration parameters: AllowInHeredoc.
@@ -151,12 +109,6 @@ Layout/TrailingWhitespace:
Exclude:
- 'spec/lib/annotate/annotate_routes_spec.rb'
-# Offense count: 2
-# Configuration parameters: AllowSafeAssignment.
-Lint/AssignmentInCondition:
- Exclude:
- - 'lib/annotate/annotate_models.rb'
-
# Offense count: 1
Lint/HandleExceptions:
Exclude:
@@ -170,12 +122,6 @@ Lint/InheritException:
Exclude:
- 'lib/annotate/annotate_models.rb'
-# Offense count: 1
-# Configuration parameters: MaximumRangeSize.
-Lint/MissingCopEnableDirective:
- Exclude:
- - 'lib/annotate/annotate_models.rb'
-
# Offense count: 2
Lint/RescueException:
Exclude:
@@ -208,7 +154,12 @@ Metrics/CyclomaticComplexity:
# Offense count: 30
# Configuration parameters: CountComments, ExcludedMethods.
Metrics/MethodLength:
- Max: 40
+ Max: 33
+
+# Offense count: 1
+# Configuration parameters: CountComments.
+Metrics/ModuleLength:
+ Max: 303
# Offense count: 9
Metrics/PerceivedComplexity:
@@ -224,6 +175,7 @@ Naming/AccessorMethodName:
# Blacklist: (?-mix:(^|\s)(EO[A-Z]{1}|END)(\s|$))
Naming/HeredocDelimiterNaming:
Exclude:
+ - 'spec/lib/annotate/annotate_models/schema_info_spec.rb'
- 'spec/lib/annotate/annotate_models_spec.rb'
- 'spec/lib/annotate/annotate_routes_spec.rb'
@@ -273,7 +225,7 @@ Style/Dir:
Exclude:
- 'bin/annotate'
-# Offense count: 10
+# Offense count: 11
Style/Documentation:
Exclude:
- 'spec/**/*'
@@ -281,6 +233,7 @@ Style/Documentation:
- 'lib/annotate.rb'
- 'lib/annotate/active_record_patch.rb'
- 'lib/annotate/annotate_models.rb'
+ - 'lib/annotate/annotate_models/schema_info.rb'
- 'lib/annotate/annotate_routes.rb'
- 'lib/annotate/annotate_routes/header_generator.rb'
- 'lib/annotate/annotate_routes/helpers.rb'
@@ -301,22 +254,14 @@ Style/ExpandPathArguments:
Exclude:
- 'annotate.gemspec'
-# Offense count: 9
-# Cop supports --auto-correct.
-# Configuration parameters: EnforcedStyle.
-# SupportedStyles: format, sprintf, percent
-Style/FormatString:
- Exclude:
- - 'lib/annotate/annotate_models.rb'
-
# Offense count: 23
# Configuration parameters: EnforcedStyle.
# SupportedStyles: annotated, template, unannotated
Style/FormatStringToken:
Exclude:
- - 'lib/annotate/annotate_models.rb'
+ - 'lib/annotate/annotate_models/schema_info.rb'
-# Offense count: 30
+# Offense count: 32
# Cop supports --auto-correct.
# Configuration parameters: EnforcedStyle.
# SupportedStyles: when_needed, always, never
@@ -338,7 +283,7 @@ Style/HashSyntax:
- 'lib/tasks/annotate_routes.rake'
- 'spec/lib/annotate/annotate_models_spec.rb'
-# Offense count: 7
+# Offense count: 5
# Cop supports --auto-correct.
Style/IfUnlessModifier:
Exclude:
@@ -382,7 +327,7 @@ Style/NestedParenthesizedCalls:
Exclude:
- 'bin/annotate'
-# Offense count: 3
+# Offense count: 2
# Cop supports --auto-correct.
# Configuration parameters: AutoCorrect, EnforcedStyle, IgnoredMethods.
# SupportedStyles: predicate, comparison
@@ -392,7 +337,7 @@ Style/NumericPredicate:
- 'lib/annotate.rb'
- 'lib/annotate/annotate_models.rb'
-# Offense count: 12
+# Offense count: 7
# Cop supports --auto-correct.
# Configuration parameters: PreferredDelimiters.
Style/PercentLiteralDelimiters:
@@ -401,7 +346,6 @@ Style/PercentLiteralDelimiters:
- 'lib/annotate/annotate_models.rb'
- 'lib/annotate/annotate_routes.rb'
- 'lib/tasks/annotate_models_migrate.rake'
- - 'spec/lib/annotate/annotate_models_spec.rb'
- 'spec/lib/tasks/annotate_models_migrate_spec.rb'
# Offense count: 1
@@ -419,12 +363,6 @@ Style/RedundantBegin:
- 'lib/annotate/annotate_models.rb'
- 'spec/lib/annotate/annotate_models_spec.rb'
-# Offense count: 1
-# Cop supports --auto-correct.
-Style/RedundantParentheses:
- Exclude:
- - 'lib/annotate/annotate_models.rb'
-
# Offense count: 1
# Cop supports --auto-correct.
# Configuration parameters: AllowMultipleReturnValues.
@@ -461,13 +399,14 @@ Style/RescueStandardError:
Exclude:
- 'lib/annotate.rb'
-# Offense count: 2
+# Offense count: 3
# Cop supports --auto-correct.
# Configuration parameters: ConvertCodeThatCanStartToReturnNil, Whitelist.
# Whitelist: present?, blank?, presence, try, try!
Style/SafeNavigation:
Exclude:
- 'lib/annotate/annotate_models.rb'
+ - 'lib/annotate/annotate_models/schema_info.rb'
# Offense count: 15
# Cop supports --auto-correct.
@@ -477,7 +416,7 @@ Style/StderrPuts:
- 'lib/annotate.rb'
- 'lib/annotate/annotate_models.rb'
-# Offense count: 55
+# Offense count: 33
# Cop supports --auto-correct.
# Configuration parameters: EnforcedStyle, ConsistentQuotesInMultiline.
# SupportedStyles: single_quotes, double_quotes
@@ -491,14 +430,6 @@ Style/StringLiterals:
- 'spec/lib/annotate/annotate_models_spec.rb'
- 'spec/lib/annotate/parser_spec.rb'
-# Offense count: 2
-# Cop supports --auto-correct.
-# Configuration parameters: EnforcedStyle.
-# SupportedStyles: single_quotes, double_quotes
-Style/StringLiteralsInInterpolation:
- Exclude:
- - 'lib/annotate/annotate_models.rb'
-
# Offense count: 8
# Cop supports --auto-correct.
# Configuration parameters: MinSize.
@@ -512,21 +443,13 @@ Style/SymbolLiteral:
Exclude:
- 'spec/lib/annotate/annotate_models_spec.rb'
-# Offense count: 3
-# Cop supports --auto-correct.
-# Configuration parameters: EnforcedStyleForMultiline.
-# SupportedStylesForMultiline: comma, consistent_comma, no_comma
-Style/TrailingCommaInArrayLiteral:
- Exclude:
- - 'spec/lib/annotate/annotate_models_spec.rb'
-
# Offense count: 2
# Cop supports --auto-correct.
Style/UnneededPercentQ:
Exclude:
- 'annotate.gemspec'
-# Offense count: 381
+# Offense count: 394
# Cop supports --auto-correct.
# Configuration parameters: AutoCorrect, AllowHeredoc, AllowURI, URISchemes, IgnoreCopDirectives, IgnoredPatterns.
# URISchemes: http, https
diff --git a/lib/annotate/annotate_models.rb b/lib/annotate/annotate_models.rb
index 469283f89..ef26087c3 100644
--- a/lib/annotate/annotate_models.rb
+++ b/lib/annotate/annotate_models.rb
@@ -1,9 +1,8 @@
-# rubocop:disable Metrics/ModuleLength
-
require 'bigdecimal'
require 'annotate/constants'
require_relative 'annotate_models/file_patterns'
+require_relative 'annotate_models/schema_info'
module AnnotateModels
# Annotate Models plugin use this header
@@ -11,34 +10,11 @@ module AnnotateModels
COMPAT_PREFIX_MD = '## Schema Info'.freeze
PREFIX = '== Schema Information'.freeze
PREFIX_MD = '## Schema Information'.freeze
- END_MARK = '== Schema Information End'.freeze
SKIP_ANNOTATION_PREFIX = '# -\*- SkipSchemaAnnotations'.freeze
MATCHED_TYPES = %w(test fixture factory serializer scaffold controller helper).freeze
- # Don't show limit (#) on these column types
- # Example: show "integer" instead of "integer(4)"
- NO_LIMIT_COL_TYPES = %w(integer bigint boolean).freeze
-
- # Don't show default value for these column types
- NO_DEFAULT_COL_TYPES = %w(json jsonb hstore).freeze
-
- INDEX_CLAUSES = {
- unique: {
- default: 'UNIQUE',
- markdown: '_unique_'
- },
- where: {
- default: 'WHERE',
- markdown: '_where_'
- },
- using: {
- default: 'USING',
- markdown: '_using_'
- }
- }.freeze
-
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
class << self
@@ -83,262 +59,6 @@ def get_patterns(options, pattern_types = [])
current_patterns
end
- # Simple quoting for the default column value
- def quote(value)
- case value
- when NilClass then 'NULL'
- when TrueClass then 'TRUE'
- when FalseClass then 'FALSE'
- when Float, Integer then value.to_s
- # BigDecimals need to be output in a non-normalized form and quoted.
- when BigDecimal then value.to_s('F')
- when Array then value.map { |v| quote(v) }
- else
- value.inspect
- end
- end
-
- def schema_default(klass, column)
- quote(klass.column_defaults[column.name])
- end
-
- def retrieve_indexes_from_table(klass)
- table_name = klass.table_name
- return [] unless table_name
-
- indexes = klass.connection.indexes(table_name)
- return indexes if indexes.any? || !klass.table_name_prefix
-
- # Try to search the table without prefix
- table_name_without_prefix = table_name.to_s.sub(klass.table_name_prefix, '')
- klass.connection.indexes(table_name_without_prefix)
- end
-
- # Use the column information in an ActiveRecord class
- # to create a comment block containing a line for
- # each column. The line contains the column name,
- # the type (and length), and any optional attributes
- def get_schema_info(klass, header, options = {})
- info = "# #{header}\n"
- info << get_schema_header_text(klass, options)
-
- max_size = max_schema_info_width(klass, options)
- md_names_overhead = 6
- md_type_allowance = 18
- bare_type_allowance = 16
-
- if options[:format_markdown]
- info << sprintf( "# %-#{max_size + md_names_overhead}.#{max_size + md_names_overhead}s | %-#{md_type_allowance}.#{md_type_allowance}s | %s\n", 'Name', 'Type', 'Attributes' )
- info << "# #{ '-' * ( max_size + md_names_overhead ) } | #{'-' * md_type_allowance} | #{ '-' * 27 }\n"
- end
-
- cols = columns(klass, options)
- cols.each do |col|
- col_type = get_col_type(col)
- attrs = get_attributes(col, col_type, klass, options)
- col_name = if with_comments?(klass, options) && col.comment
- "#{col.name}(#{col.comment.gsub(/\n/, "\\n")})"
- else
- col.name
- end
-
- if options[:format_rdoc]
- info << sprintf("# %-#{max_size}.#{max_size}s%s", "*#{col_name}*::", attrs.unshift(col_type).join(", ")).rstrip + "\n"
- elsif options[:format_yard]
- info << sprintf("# @!attribute #{col_name}") + "\n"
- 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)
- info << sprintf("# @return [#{ruby_class}]") + "\n"
- elsif options[:format_markdown]
- name_remainder = max_size - col_name.length - non_ascii_length(col_name)
- type_remainder = (md_type_allowance - 2) - col_type.length
- info << (sprintf("# **`%s`**%#{name_remainder}s | `%s`%#{type_remainder}s | `%s`", col_name, " ", col_type, " ", attrs.join(", ").rstrip)).gsub('``', ' ').rstrip + "\n"
- else
- info << format_default(col_name, max_size, col_type, bare_type_allowance, attrs)
- end
- end
-
- if options[:show_indexes] && klass.table_exists?
- info << get_index_info(klass, options)
- end
-
- if options[:show_foreign_keys] && klass.table_exists?
- info << get_foreign_key_info(klass, options)
- end
-
- info << get_schema_footer_text(klass, options)
- end
-
- def get_schema_header_text(klass, options = {})
- info = "#\n"
- if options[:format_markdown]
- info << "# Table name: `#{klass.table_name}`\n"
- info << "#\n"
- info << "# ### Columns\n"
- else
- info << "# Table name: #{klass.table_name}\n"
- end
- info << "#\n"
- end
-
- def get_schema_footer_text(_klass, options = {})
- info = ''
- if options[:format_rdoc]
- info << "#--\n"
- info << "# #{END_MARK}\n"
- info << "#++\n"
- else
- info << "#\n"
- end
- end
-
- def get_index_info(klass, options = {})
- index_info = if options[:format_markdown]
- "#\n# ### Indexes\n#\n"
- else
- "#\n# Indexes\n#\n"
- end
-
- indexes = retrieve_indexes_from_table(klass)
- return '' if indexes.empty?
-
- max_size = indexes.collect{|index| index.name.size}.max + 1
- indexes.sort_by(&:name).each do |index|
- index_info << if options[:format_markdown]
- final_index_string_in_markdown(index)
- else
- final_index_string(index, max_size)
- end
- end
-
- index_info
- end
-
- def get_col_type(col)
- if (col.respond_to?(:bigint?) && col.bigint?) || /\Abigint\b/ =~ col.sql_type
- 'bigint'
- else
- (col.type || col.sql_type).to_s
- end
- end
-
- def index_columns_info(index)
- Array(index.columns).map do |col|
- if index.try(:orders) && index.orders[col.to_s]
- "#{col} #{index.orders[col.to_s].upcase}"
- else
- col.to_s.gsub("\r", '\r').gsub("\n", '\n')
- end
- end
- end
-
- def index_unique_info(index, format = :default)
- index.unique ? " #{INDEX_CLAUSES[:unique][format]}" : ''
- end
-
- def index_where_info(index, format = :default)
- value = index.try(:where).try(:to_s)
- if value.blank?
- ''
- else
- " #{INDEX_CLAUSES[:where][format]} #{value}"
- end
- end
-
- def index_using_info(index, format = :default)
- value = index.try(:using) && index.using.try(:to_sym)
- if !value.blank? && value != :btree
- " #{INDEX_CLAUSES[:using][format]} #{value}"
- else
- ''
- end
- end
-
- def final_index_string_in_markdown(index)
- details = sprintf(
- "%s%s%s",
- index_unique_info(index, :markdown),
- index_where_info(index, :markdown),
- index_using_info(index, :markdown)
- ).strip
- details = " (#{details})" unless details.blank?
-
- sprintf(
- "# * `%s`%s:\n# * **`%s`**\n",
- index.name,
- details,
- index_columns_info(index).join("`**\n# * **`")
- )
- end
-
- def final_index_string(index, max_size)
- sprintf(
- "# %-#{max_size}.#{max_size}s %s%s%s%s",
- index.name,
- "(#{index_columns_info(index).join(',')})",
- index_unique_info(index),
- index_where_info(index),
- index_using_info(index)
- ).rstrip + "\n"
- end
-
- def hide_limit?(col_type, options)
- excludes =
- if options[:hide_limit_column_types].blank?
- NO_LIMIT_COL_TYPES
- else
- options[:hide_limit_column_types].split(',')
- end
-
- excludes.include?(col_type)
- end
-
- def hide_default?(col_type, options)
- excludes =
- if options[:hide_default_column_types].blank?
- NO_DEFAULT_COL_TYPES
- else
- options[:hide_default_column_types].split(',')
- end
-
- excludes.include?(col_type)
- end
-
- def get_foreign_key_info(klass, options = {})
- fk_info = if options[:format_markdown]
- "#\n# ### Foreign Keys\n#\n"
- else
- "#\n# Foreign Keys\n#\n"
- end
-
- return '' unless klass.connection.respond_to?(:supports_foreign_keys?) &&
- klass.connection.supports_foreign_keys? && klass.connection.respond_to?(:foreign_keys)
-
- foreign_keys = klass.connection.foreign_keys(klass.table_name)
- return '' if foreign_keys.empty?
-
- format_name = lambda do |fk|
- return fk.options[:column] if fk.name.blank?
- options[:show_complete_foreign_keys] ? fk.name : fk.name.gsub(/(?<=^fk_rails_)[0-9a-f]{10}$/, '...')
- end
-
- max_size = foreign_keys.map(&format_name).map(&:size).max + 1
- foreign_keys.sort_by {|fk| [format_name.call(fk), fk.column]}.each do |fk|
- ref_info = "#{fk.column} => #{fk.to_table}.#{fk.primary_key}"
- constraints_info = ''
- constraints_info += "ON DELETE => #{fk.on_delete} " if fk.on_delete
- constraints_info += "ON UPDATE => #{fk.on_update} " if fk.on_update
- constraints_info.strip!
-
- fk_info << if options[:format_markdown]
- sprintf("# * `%s`%s:\n# * **`%s`**\n", format_name.call(fk), constraints_info.blank? ? '' : " (_#{constraints_info}_)", ref_info)
- else
- sprintf("# %-#{max_size}.#{max_size}s %s %s", format_name.call(fk), "(#{ref_info})", constraints_info).rstrip + "\n"
- end
- end
-
- fk_info
- end
-
# Add a schema block to a file. If the file already contains
# a schema info block (a comment starting with "== Schema Information"),
# check if it matches the block that is already there. If so, leave it be.
@@ -465,7 +185,7 @@ def matched_types(options)
def annotate(klass, file, header, options = {})
begin
klass.reset_column_information
- info = get_schema_info(klass, header, options)
+ info = SchemaInfo.generate(klass, header, options)
model_name = klass.name.underscore
table_name = klass.table_name
model_file_name = File.join(file)
@@ -713,185 +433,6 @@ def resolve_filename(filename_template, model_name, table_name)
.gsub('%PLURALIZED_MODEL_NAME%', model_name.pluralize)
.gsub('%TABLE_NAME%', table_name || model_name.pluralize)
end
-
- def classified_sort(cols)
- rest_cols = []
- timestamps = []
- associations = []
- id = nil
-
- cols.each do |c|
- if c.name.eql?('id')
- id = c
- elsif c.name.eql?('created_at') || c.name.eql?('updated_at')
- timestamps << c
- elsif c.name[-3, 3].eql?('_id')
- associations << c
- else
- rest_cols << c
- end
- end
- [rest_cols, timestamps, associations].each { |a| a.sort_by!(&:name) }
-
- ([id] << rest_cols << timestamps << associations).flatten.compact
- end
-
- private
-
- def with_comments?(klass, options)
- options[:with_comment] &&
- klass.columns.first.respond_to?(:comment) &&
- klass.columns.any? { |col| !col.comment.nil? }
- end
-
- def max_schema_info_width(klass, options)
- cols = columns(klass, options)
-
- if with_comments?(klass, options)
- max_size = cols.map do |column|
- column.name.size + (column.comment ? width(column.comment) : 0)
- end.max || 0
- max_size += 2
- else
- max_size = cols.map(&:name).map(&:size).max
- end
- max_size += options[:format_rdoc] ? 5 : 1
-
- max_size
- end
-
- def format_default(col_name, max_size, col_type, bare_type_allowance, attrs)
- sprintf("# %s:%s %s", mb_chars_ljust(col_name, max_size), mb_chars_ljust(col_type, bare_type_allowance), attrs.join(", ")).rstrip + "\n"
- end
-
- def width(string)
- string.chars.inject(0) { |acc, elem| acc + (elem.bytesize == 3 ? 2 : 1) }
- end
-
- def mb_chars_ljust(string, length)
- string = string.to_s
- padding = length - width(string)
- if padding > 0
- string + (' ' * padding)
- else
- string[0..length-1]
- end
- end
-
- def non_ascii_length(string)
- string.to_s.chars.reject(&:ascii_only?).length
- end
-
- def map_col_type_to_ruby_classes(col_type)
- case col_type
- when 'integer' then Integer.to_s
- when 'float' then Float.to_s
- when 'decimal' then BigDecimal.to_s
- when 'datetime', 'timestamp', 'time' then Time.to_s
- when 'date' then Date.to_s
- when 'text', 'string', 'binary', 'inet', 'uuid' then String.to_s
- when 'json', 'jsonb' then Hash.to_s
- when 'boolean' then 'Boolean'
- end
- end
-
- def columns(klass, options)
- cols = klass.columns
- cols += translated_columns(klass)
-
- if ignore_columns = options[:ignore_columns]
- cols = cols.reject do |col|
- col.name.match(/#{ignore_columns}/)
- end
- end
-
- cols = cols.sort_by(&:name) if options[:sort]
- cols = classified_sort(cols) if options[:classified_sort]
-
- cols
- end
-
- ##
- # Add columns managed by the globalize gem if this gem is being used.
- def translated_columns(klass)
- return [] unless klass.respond_to? :translation_class
-
- ignored_cols = ignored_translation_table_colums(klass)
- klass.translation_class.columns.reject do |col|
- ignored_cols.include? col.name.to_sym
- end
- end
-
- ##
- # These are the columns that the globalize gem needs to work but
- # are not necessary for the models to be displayed as annotations.
- def ignored_translation_table_colums(klass)
- # Construct the foreign column name in the translations table
- # eg. Model: Car, foreign column name: car_id
- foreign_column_name = [
- klass.translation_class.to_s
- .gsub('::Translation', '').gsub('::', '_')
- .downcase,
- '_id'
- ].join.to_sym
-
- [
- :id,
- :created_at,
- :updated_at,
- :locale,
- foreign_column_name
- ]
- end
-
- ##
- # Get the list of attributes that should be included in the annotation for
- # a given column.
- def get_attributes(column, column_type, klass, options)
- attrs = []
- attrs << "default(#{schema_default(klass, column)})" unless column.default.nil? || hide_default?(column_type, options)
- attrs << 'unsigned' if column.respond_to?(:unsigned?) && column.unsigned?
- attrs << 'not null' unless column.null
- 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)
-
- if column_type == 'decimal'
- column_type << "(#{column.precision}, #{column.scale})"
- elsif !%w[spatial geometry geography].include?(column_type)
- if column.limit && !options[:format_yard]
- if column.limit.is_a? Array
- attrs << "(#{column.limit.join(', ')})"
- else
- column_type << "(#{column.limit})" unless hide_limit?(column_type, options)
- end
- end
- end
-
- # Check out if we got an array column
- attrs << 'is an Array' if column.respond_to?(:array) && column.array
-
- # Check out if we got a geometric column
- # and print the type and SRID
- if column.respond_to?(:geometry_type)
- attrs << "#{column.geometry_type}, #{column.srid}"
- elsif column.respond_to?(:geometric_type) && column.geometric_type.present?
- attrs << "#{column.geometric_type.to_s.downcase}, #{column.srid}"
- end
-
- # Check if the column has indices and print "indexed" if true
- # If the index includes another column, print it too.
- if options[:simple_indexes] && klass.table_exists?# Check out if this column is indexed
- indices = retrieve_indexes_from_table(klass)
- if indices = indices.select { |ind| ind.columns.include? column.name }
- indices.sort_by(&:name).each do |ind|
- next if ind.columns.is_a?(String)
- ind = ind.columns.reject! { |i| i == column.name }
- attrs << (ind.empty? ? "indexed" : "indexed => [#{ind.join(", ")}]")
- end
- end
- end
-
- attrs
- end
end
class BadModelFileError < LoadError
diff --git a/lib/annotate/annotate_models/schema_info.rb b/lib/annotate/annotate_models/schema_info.rb
new file mode 100644
index 000000000..caf434338
--- /dev/null
+++ b/lib/annotate/annotate_models/schema_info.rb
@@ -0,0 +1,477 @@
+module AnnotateModels
+ module SchemaInfo # rubocop:disable Metrics/ModuleLength
+ # Don't show default value for these column types
+ NO_DEFAULT_COL_TYPES = %w[json jsonb hstore].freeze
+
+ # Don't show limit (#) on these column types
+ # Example: show "integer" instead of "integer(4)"
+ NO_LIMIT_COL_TYPES = %w[integer bigint boolean].freeze
+
+ INDEX_CLAUSES = {
+ unique: {
+ default: 'UNIQUE',
+ markdown: '_unique_'
+ },
+ where: {
+ default: 'WHERE',
+ markdown: '_where_'
+ },
+ using: {
+ default: 'USING',
+ markdown: '_using_'
+ }
+ }.freeze
+
+ END_MARK = '== Schema Information End'.freeze
+
+ class << self
+ # Use the column information in an ActiveRecord class
+ # to create a comment block containing a line for
+ # each column. The line contains the column name,
+ # the type (and length), and any optional attributes
+ def generate(klass, header, options = {}) # rubocop:disable Metrics/MethodLength
+ info = "# #{header}\n"
+ info << get_schema_header_text(klass, options)
+
+ max_size = max_schema_info_width(klass, options)
+ md_names_overhead = 6
+ md_type_allowance = 18
+ bare_type_allowance = 16
+
+ if options[:format_markdown]
+ info << format("# %-#{max_size + md_names_overhead}.#{max_size + md_names_overhead}s | %-#{md_type_allowance}.#{md_type_allowance}s | %s\n",
+ 'Name',
+ 'Type',
+ 'Attributes')
+ info << "# #{'-' * (max_size + md_names_overhead)} | #{'-' * md_type_allowance} | #{'-' * 27}\n"
+ end
+
+ cols = columns(klass, options)
+ cols.each do |col|
+ col_type = get_col_type(col)
+ attrs = get_attributes(col, col_type, klass, options)
+ col_name = if with_comments?(klass, options) && col.comment
+ "#{col.name}(#{col.comment.gsub(/\n/, '\\n')})"
+ else
+ col.name
+ end
+
+ if options[:format_rdoc]
+ info << format("# %-#{max_size}.#{max_size}s%s",
+ "*#{col_name}*::",
+ attrs.unshift(col_type).join(', ')).rstrip + "\n"
+ elsif options[:format_yard]
+ info << sprintf("# @!attribute #{col_name}") + "\n"
+ 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)
+ info << sprintf("# @return [#{ruby_class}]") + "\n"
+ elsif options[:format_markdown]
+ name_remainder = max_size - col_name.length - non_ascii_length(col_name)
+ type_remainder = (md_type_allowance - 2) - col_type.length
+ info << format("# **`%s`**%#{name_remainder}s | `%s`%#{type_remainder}s | `%s`",
+ col_name,
+ ' ',
+ col_type,
+ ' ',
+ attrs.join(', ').rstrip).gsub('``', ' ').rstrip + "\n"
+ else
+ info << format_default(col_name, max_size, col_type, bare_type_allowance, attrs)
+ end
+ end
+
+ info << get_index_info(klass, options) if options[:show_indexes] && klass.table_exists?
+
+ info << get_foreign_key_info(klass, options) if options[:show_foreign_keys] && klass.table_exists?
+
+ info << get_schema_footer_text(klass, options)
+ end
+
+ private
+
+ def get_schema_header_text(klass, options = {})
+ info = "#\n"
+ if options[:format_markdown]
+ info << "# Table name: `#{klass.table_name}`\n"
+ info << "#\n"
+ info << "# ### Columns\n"
+ else
+ info << "# Table name: #{klass.table_name}\n"
+ end
+ info << "#\n"
+ end
+
+ def max_schema_info_width(klass, options)
+ cols = columns(klass, options)
+
+ if with_comments?(klass, options)
+ max_size = cols.map do |column|
+ column.name.size + (column.comment ? width(column.comment) : 0)
+ end.max || 0
+ max_size += 2
+ else
+ max_size = cols.map(&:name).map(&:size).max
+ end
+ max_size += options[:format_rdoc] ? 5 : 1
+
+ max_size
+ end
+
+ def with_comments?(klass, options)
+ options[:with_comment] &&
+ klass.columns.first.respond_to?(:comment) &&
+ klass.columns.map(&:comment).any? { |comment| !comment.nil? }
+ end
+
+ def width(string)
+ string.chars.inject(0) { |acc, elem| acc + (elem.bytesize == 3 ? 2 : 1) }
+ end
+
+ def columns(klass, options)
+ cols = klass.columns
+ cols += translated_columns(klass)
+
+ ignore_columns = options[:ignore_columns]
+ if ignore_columns
+ cols = cols.reject do |col|
+ col.name.match(/#{ignore_columns}/)
+ end
+ end
+
+ cols = cols.sort_by(&:name) if options[:sort]
+ cols = classified_sort(cols) if options[:classified_sort]
+
+ cols
+ end
+
+ # Add columns managed by the globalize gem if this gem is being used.
+ def translated_columns(klass)
+ return [] unless klass.respond_to? :translation_class
+
+ ignored_cols = ignored_translation_table_colums(klass)
+ klass.translation_class.columns.reject do |col|
+ ignored_cols.include? col.name.to_sym
+ end
+ end
+
+ # These are the columns that the globalize gem needs to work but
+ # are not necessary for the models to be displayed as annotations.
+ def ignored_translation_table_colums(klass)
+ # Construct the foreign column name in the translations table
+ # eg. Model: Car, foreign column name: car_id
+ foreign_column_name = [
+ klass.translation_class.to_s
+ .gsub('::Translation', '').gsub('::', '_')
+ .downcase,
+ '_id'
+ ].join.to_sym
+
+ [
+ :id,
+ :created_at,
+ :updated_at,
+ :locale,
+ foreign_column_name
+ ]
+ end
+
+ def classified_sort(cols)
+ rest_cols = []
+ timestamps = []
+ associations = []
+ id = nil
+
+ cols.each do |c|
+ if c.name.eql?('id')
+ id = c
+ elsif c.name.eql?('created_at') || c.name.eql?('updated_at')
+ timestamps << c
+ elsif c.name[-3, 3].eql?('_id')
+ associations << c
+ else
+ rest_cols << c
+ end
+ end
+ [rest_cols, timestamps, associations].each { |a| a.sort_by!(&:name) }
+
+ ([id] << rest_cols << timestamps << associations).flatten.compact
+ end
+
+ def get_col_type(col)
+ if (col.respond_to?(:bigint?) && col.bigint?) || /\Abigint\b/ =~ col.sql_type
+ 'bigint'
+ else
+ (col.type || col.sql_type).to_s
+ end
+ end
+
+ # Get the list of attributes that should be included in the annotation for
+ # a given column.
+ def get_attributes(column, column_type, klass, options)
+ attrs = []
+ attrs << "default(#{schema_default(klass, column)})" unless column.default.nil? || hide_default?(column_type, options)
+ attrs << 'unsigned' if column.respond_to?(:unsigned?) && column.unsigned?
+ attrs << 'not null' unless column.null
+ 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)
+
+ if column_type == 'decimal'
+ column_type << "(#{column.precision}, #{column.scale})"
+ elsif !%w[spatial geometry geography].include?(column_type)
+ if column.limit && !options[:format_yard]
+ if column.limit.is_a? Array
+ attrs << "(#{column.limit.join(', ')})"
+ else
+ column_type << "(#{column.limit})" unless hide_limit?(column_type, options)
+ end
+ end
+ end
+
+ # Check out if we got an array column
+ attrs << 'is an Array' if column.respond_to?(:array) && column.array
+
+ # Check out if we got a geometric column
+ # and print the type and SRID
+ if column.respond_to?(:geometry_type)
+ attrs << "#{column.geometry_type}, #{column.srid}"
+ elsif column.respond_to?(:geometric_type) && column.geometric_type.present?
+ attrs << "#{column.geometric_type.to_s.downcase}, #{column.srid}"
+ end
+
+ # Check if the column has indices and print "indexed" if true
+ # If the index includes another column, print it too.
+ if options[:simple_indexes] && klass.table_exists? # Check out if this column is indexed
+ indices = retrieve_indexes_from_table(klass).select { |ind| ind.columns.include? column.name }
+ indices&.sort_by(&:name)&.each do |ind|
+ next if ind.columns.is_a?(String)
+
+ ind = ind.columns.reject! { |i| i == column.name }
+ attrs << (ind.empty? ? 'indexed' : "indexed => [#{ind.join(', ')}]")
+ end
+ end
+
+ attrs
+ end
+
+ def schema_default(klass, column)
+ quote(klass.column_defaults[column.name])
+ end
+
+ # Simple quoting for the default column value
+ def quote(value)
+ case value
+ when NilClass then 'NULL'
+ when TrueClass then 'TRUE'
+ when FalseClass then 'FALSE'
+ when Float, Integer then value.to_s
+ # BigDecimals need to be output in a non-normalized form and quoted.
+ when BigDecimal then value.to_s('F')
+ when Array then value.map { |v| quote(v) }
+ else
+ value.inspect
+ end
+ end
+
+ def hide_default?(col_type, options)
+ excludes =
+ if options[:hide_default_column_types].blank?
+ NO_DEFAULT_COL_TYPES
+ else
+ options[:hide_default_column_types].split(',')
+ end
+
+ excludes.include?(col_type)
+ end
+
+ def hide_limit?(col_type, options)
+ excludes =
+ if options[:hide_limit_column_types].blank?
+ NO_LIMIT_COL_TYPES
+ else
+ options[:hide_limit_column_types].split(',')
+ end
+
+ excludes.include?(col_type)
+ end
+
+ def retrieve_indexes_from_table(klass)
+ table_name = klass.table_name
+ return [] unless table_name
+
+ indexes = klass.connection.indexes(table_name)
+ return indexes if indexes.any? || !klass.table_name_prefix
+
+ # Try to search the table without prefix
+ table_name_without_prefix = table_name.to_s.sub(klass.table_name_prefix, '')
+ klass.connection.indexes(table_name_without_prefix)
+ end
+
+ def map_col_type_to_ruby_classes(col_type)
+ case col_type
+ when 'integer' then Integer.to_s
+ when 'float' then Float.to_s
+ when 'decimal' then BigDecimal.to_s
+ when 'datetime', 'timestamp', 'time' then Time.to_s
+ when 'date' then Date.to_s
+ when 'text', 'string', 'binary', 'inet', 'uuid' then String.to_s
+ when 'json', 'jsonb' then Hash.to_s
+ when 'boolean' then 'Boolean'
+ end
+ end
+
+ def non_ascii_length(string)
+ string.to_s.chars.reject(&:ascii_only?).length
+ end
+
+ def format_default(col_name, max_size, col_type, bare_type_allowance, attrs)
+ format('# %s:%s %s',
+ mb_chars_ljust(col_name, max_size),
+ mb_chars_ljust(col_type, bare_type_allowance),
+ attrs.join(', ')).rstrip + "\n"
+ end
+
+ def mb_chars_ljust(string, length)
+ string = string.to_s
+ padding = length - width(string)
+ if padding.positive?
+ string + (' ' * padding)
+ else
+ string[0..(length - 1)]
+ end
+ end
+
+ def get_index_info(klass, options = {})
+ index_info = if options[:format_markdown]
+ "#\n# ### Indexes\n#\n"
+ else
+ "#\n# Indexes\n#\n"
+ end
+
+ indexes = retrieve_indexes_from_table(klass)
+ return '' if indexes.empty?
+
+ max_size = indexes.collect { |index| index.name.size }.max + 1
+ indexes.sort_by(&:name).each do |index|
+ index_info << if options[:format_markdown]
+ final_index_string_in_markdown(index)
+ else
+ final_index_string(index, max_size)
+ end
+ end
+
+ index_info
+ end
+
+ def final_index_string_in_markdown(index)
+ details = format(
+ '%s%s%s',
+ index_unique_info(index, :markdown),
+ index_where_info(index, :markdown),
+ index_using_info(index, :markdown)
+ ).strip
+ details = " (#{details})" unless details.blank?
+
+ format(
+ "# * `%s`%s:\n# * **`%s`**\n",
+ index.name,
+ details,
+ index_columns_info(index).join("`**\n# * **`")
+ )
+ end
+
+ def final_index_string(index, max_size)
+ format(
+ "# %-#{max_size}.#{max_size}s %s%s%s%s",
+ index.name,
+ "(#{index_columns_info(index).join(',')})",
+ index_unique_info(index),
+ index_where_info(index),
+ index_using_info(index)
+ ).rstrip + "\n"
+ end
+
+ def index_columns_info(index)
+ Array(index.columns).map do |col|
+ if index.try(:orders) && index.orders[col.to_s]
+ "#{col} #{index.orders[col.to_s].upcase}"
+ else
+ col.to_s.gsub("\r", '\r').gsub("\n", '\n')
+ end
+ end
+ end
+
+ def index_where_info(index, format = :default)
+ value = index.try(:where).try(:to_s)
+ if value.blank?
+ ''
+ else
+ " #{INDEX_CLAUSES[:where][format]} #{value}"
+ end
+ end
+
+ def index_unique_info(index, format = :default)
+ index.unique ? " #{INDEX_CLAUSES[:unique][format]}" : ''
+ end
+
+ def index_using_info(index, format = :default)
+ value = index.try(:using) && index.using.try(:to_sym)
+ if !value.blank? && value != :btree
+ " #{INDEX_CLAUSES[:using][format]} #{value}"
+ else
+ ''
+ end
+ end
+
+ def get_foreign_key_info(klass, options = {})
+ fk_info = if options[:format_markdown]
+ "#\n# ### Foreign Keys\n#\n"
+ else
+ "#\n# Foreign Keys\n#\n"
+ end
+
+ return '' unless klass.connection.respond_to?(:supports_foreign_keys?) &&
+ klass.connection.supports_foreign_keys? && klass.connection.respond_to?(:foreign_keys)
+
+ foreign_keys = klass.connection.foreign_keys(klass.table_name)
+ return '' if foreign_keys.empty?
+
+ format_name = lambda do |fk|
+ return fk.options[:column] if fk.name.blank?
+
+ options[:show_complete_foreign_keys] ? fk.name : fk.name.gsub(/(?<=^fk_rails_)[0-9a-f]{10}$/, '...')
+ end
+
+ max_size = foreign_keys.map(&format_name).map(&:size).max + 1
+ foreign_keys.sort_by { |fk| [format_name.call(fk), fk.column] }.each do |fk|
+ ref_info = "#{fk.column} => #{fk.to_table}.#{fk.primary_key}"
+ constraints_info = ''
+ constraints_info += "ON DELETE => #{fk.on_delete} " if fk.on_delete
+ constraints_info += "ON UPDATE => #{fk.on_update} " if fk.on_update
+ constraints_info.strip!
+
+ fk_info << if options[:format_markdown]
+ format("# * `%s`%s:\n# * **`%s`**\n",
+ format_name.call(fk),
+ constraints_info.blank? ? '' : " (_#{constraints_info}_)",
+ ref_info)
+ else
+ format("# %-#{max_size}.#{max_size}s %s %s",
+ format_name.call(fk),
+ "(#{ref_info})",
+ constraints_info).rstrip + "\n"
+ end
+ end
+
+ fk_info
+ end
+
+ def get_schema_footer_text(_klass, options = {})
+ info = ''
+ if options[:format_rdoc]
+ info << "#--\n"
+ info << "# #{END_MARK}\n"
+ info << "#++\n"
+ else
+ info << "#\n"
+ end
+ end
+ end
+ end
+end
diff --git a/lib/generators/annotate/templates/auto_annotate_models.rake b/lib/generators/annotate/templates/auto_annotate_models.rake
index 1f355249b..5b9318d15 100644
--- a/lib/generators/annotate/templates/auto_annotate_models.rake
+++ b/lib/generators/annotate/templates/auto_annotate_models.rake
@@ -37,8 +37,8 @@ if Rails.env.development?
'ignore_columns' => nil,
'ignore_routes' => nil,
'ignore_unknown_models' => 'false',
- 'hide_limit_column_types' => '<%= AnnotateModels::NO_LIMIT_COL_TYPES.join(",") %>',
- 'hide_default_column_types' => '<%= AnnotateModels::NO_DEFAULT_COL_TYPES.join(",") %>',
+ 'hide_limit_column_types' => '<%= AnnotateModels::SchemaInfo::NO_LIMIT_COL_TYPES.join(",") %>',
+ 'hide_default_column_types' => '<%= AnnotateModels::SchemaInfo::NO_DEFAULT_COL_TYPES.join(",") %>',
'skip_on_db_migrate' => 'false',
'format_bare' => 'true',
'format_rdoc' => 'false',
diff --git a/spec/lib/annotate/annotate_models/schema_info_spec.rb b/spec/lib/annotate/annotate_models/schema_info_spec.rb
new file mode 100644
index 000000000..3a844792d
--- /dev/null
+++ b/spec/lib/annotate/annotate_models/schema_info_spec.rb
@@ -0,0 +1,1497 @@
+require_relative '../../../spec_helper'
+require 'annotate/annotate_models'
+
+describe AnnotateModels::SchemaInfo do
+ describe '.generate' do
+ subject do
+ AnnotateModels::SchemaInfo.generate(klass, header, **options)
+ end
+
+ let :klass do
+ mock_class(:users, primary_key, columns, indexes, foreign_keys)
+ end
+
+ let :indexes do
+ []
+ end
+
+ let :foreign_keys do
+ []
+ end
+
+ context 'when option is not present' do
+ let :options do
+ {}
+ end
+
+ context 'when header is "Schema Info"' do
+ let :header do
+ 'Schema Info'
+ end
+
+ context 'when the primary key is not specified' do
+ let :primary_key do
+ nil
+ end
+
+ context 'when the columns are normal' do
+ let :columns do
+ [
+ mock_column(:id, :integer),
+ mock_column(:name, :string, limit: 50)
+ ]
+ end
+
+ let :expected_result do
+ <<~EOS
+ # Schema Info
+ #
+ # Table name: users
+ #
+ # id :integer not null
+ # name :string(50) not null
+ #
+ EOS
+ end
+
+ it 'returns schema info' do
+ is_expected.to eq(expected_result)
+ end
+ end
+
+ context 'when an enum column exists' do
+ let :columns do
+ [
+ mock_column(:id, :integer),
+ mock_column(:name, :enum, limit: [:enum1, :enum2])
+ ]
+ end
+
+ let :expected_result do
+ <<~EOS
+ # Schema Info
+ #
+ # Table name: users
+ #
+ # id :integer not null
+ # name :enum not null, (enum1, enum2)
+ #
+ EOS
+ end
+
+ it 'returns schema info' do
+ is_expected.to eq(expected_result)
+ end
+ end
+
+ context 'when unsigned columns exist' do
+ let :columns do
+ [
+ mock_column(:id, :integer),
+ mock_column(:integer, :integer, unsigned?: true),
+ mock_column(:bigint, :integer, unsigned?: true, bigint?: true),
+ mock_column(:bigint, :bigint, unsigned?: true),
+ mock_column(:float, :float, unsigned?: true),
+ mock_column(:decimal, :decimal, unsigned?: true, precision: 10, scale: 2)
+ ]
+ end
+
+ let :expected_result do
+ <<~EOS
+ # Schema Info
+ #
+ # Table name: users
+ #
+ # id :integer not null
+ # integer :integer unsigned, not null
+ # bigint :bigint unsigned, not null
+ # bigint :bigint unsigned, not null
+ # float :float unsigned, not null
+ # decimal :decimal(10, 2) unsigned, not null
+ #
+ EOS
+ end
+
+ it 'returns schema info' do
+ is_expected.to eq(expected_result)
+ end
+ end
+ end
+
+ context 'when the primary key is specified' do
+ context 'when the primary_key is :id' do
+ let :primary_key do
+ :id
+ end
+
+ context 'when columns are normal' do
+ let :columns do
+ [
+ mock_column(:id, :integer, limit: 8),
+ mock_column(:name, :string, limit: 50),
+ mock_column(:notes, :text, limit: 55)
+ ]
+ end
+
+ let :expected_result do
+ <<~EOS
+ # Schema Info
+ #
+ # Table name: users
+ #
+ # id :integer not null, primary key
+ # name :string(50) not null
+ # notes :text(55) not null
+ #
+ EOS
+ end
+
+ it 'returns schema info' do
+ is_expected.to eq(expected_result)
+ end
+ end
+
+ context 'when columns have default values' do
+ let :columns do
+ [
+ mock_column(:id, :integer),
+ mock_column(:size, :integer, default: 20),
+ mock_column(:flag, :boolean, default: false)
+ ]
+ end
+
+ let :expected_result do
+ <<~EOS
+ # Schema Info
+ #
+ # Table name: users
+ #
+ # id :integer not null, primary key
+ # size :integer default(20), not null
+ # flag :boolean default(FALSE), not null
+ #
+ EOS
+ end
+
+ it 'returns schema info with default values' do
+ is_expected.to eq(expected_result)
+ end
+ end
+
+ context 'with Globalize gem' do
+ let :translation_klass do
+ double('Post::Translation',
+ to_s: 'Post::Translation',
+ columns: [
+ mock_column(:id, :integer, limit: 8),
+ mock_column(:post_id, :integer, limit: 8),
+ mock_column(:locale, :string, limit: 50),
+ mock_column(:title, :string, limit: 50)
+ ])
+ end
+
+ let :klass do
+ mock_class(:posts, primary_key, columns, indexes, foreign_keys).tap do |mock_klass|
+ allow(mock_klass).to receive(:translation_class).and_return(translation_klass)
+ end
+ end
+
+ let :columns do
+ [
+ mock_column(:id, :integer, limit: 8),
+ mock_column(:author_name, :string, limit: 50)
+ ]
+ end
+
+ let :expected_result do
+ <<~EOS
+ # Schema Info
+ #
+ # Table name: posts
+ #
+ # id :integer not null, primary key
+ # author_name :string(50) not null
+ # title :string(50) not null
+ #
+ EOS
+ end
+
+ it 'returns schema info' do
+ is_expected.to eq expected_result
+ end
+ end
+ end
+
+ context 'when the primary key is an array (using composite_primary_keys)' do
+ let :primary_key do
+ [:a_id, :b_id]
+ end
+
+ let :columns do
+ [
+ mock_column(:a_id, :integer),
+ mock_column(:b_id, :integer),
+ mock_column(:name, :string, limit: 50)
+ ]
+ end
+
+ let :expected_result do
+ <<~EOS
+ # Schema Info
+ #
+ # Table name: users
+ #
+ # a_id :integer not null, primary key
+ # b_id :integer not null, primary key
+ # name :string(50) not null
+ #
+ EOS
+ end
+
+ it 'returns schema info' do
+ is_expected.to eq(expected_result)
+ end
+ end
+ end
+ end
+ end
+
+ context 'when option is present' do
+ context 'when header is "Schema Info"' do
+ let :header do
+ 'Schema Info'
+ end
+
+ context 'when the primary key is specified' do
+ context 'when the primary_key is :id' do
+ let :primary_key do
+ :id
+ end
+
+ context 'when indexes exist' do
+ context 'when option "show_indexes" is true' do
+ let :options do
+ { show_indexes: true }
+ end
+
+ context 'when indexes are normal' do
+ let :columns do
+ [
+ mock_column(:id, :integer),
+ mock_column(:foreign_thing_id, :integer)
+ ]
+ end
+
+ let :indexes do
+ [
+ mock_index('index_rails_02e851e3b7', columns: ['id']),
+ mock_index('index_rails_02e851e3b8', columns: ['foreign_thing_id'])
+ ]
+ end
+
+ let :expected_result do
+ <<~EOS
+ # Schema Info
+ #
+ # Table name: users
+ #
+ # id :integer not null, primary key
+ # foreign_thing_id :integer not null
+ #
+ # Indexes
+ #
+ # index_rails_02e851e3b7 (id)
+ # index_rails_02e851e3b8 (foreign_thing_id)
+ #
+ EOS
+ end
+
+ it 'returns schema info with index information' do
+ is_expected.to eq expected_result
+ end
+ end
+
+ context 'when one of indexes includes orderd index key' do
+ let :columns do
+ [
+ mock_column('id', :integer),
+ mock_column('firstname', :string),
+ mock_column('surname', :string),
+ mock_column('value', :string)
+ ]
+ end
+
+ let :indexes do
+ [
+ mock_index('index_rails_02e851e3b7', columns: ['id']),
+ mock_index('index_rails_02e851e3b8',
+ columns: %w[firstname surname value],
+ orders: { 'surname' => :asc, 'value' => :desc })
+ ]
+ end
+
+ let :expected_result do
+ <<~EOS
+ # Schema Info
+ #
+ # Table name: users
+ #
+ # id :integer not null, primary key
+ # firstname :string not null
+ # surname :string not null
+ # value :string not null
+ #
+ # Indexes
+ #
+ # index_rails_02e851e3b7 (id)
+ # index_rails_02e851e3b8 (firstname,surname ASC,value DESC)
+ #
+ EOS
+ end
+
+ it 'returns schema info with index information' do
+ is_expected.to eq expected_result
+ end
+ end
+
+ context 'when one of indexes includes "where" clause' do
+ let :columns do
+ [
+ mock_column('id', :integer),
+ mock_column('firstname', :string),
+ mock_column('surname', :string),
+ mock_column('value', :string)
+ ]
+ end
+
+ let :indexes do
+ [
+ mock_index('index_rails_02e851e3b7', columns: ['id']),
+ mock_index('index_rails_02e851e3b8',
+ columns: %w[firstname surname],
+ where: 'value IS NOT NULL')
+ ]
+ end
+
+ let :expected_result do
+ <<~EOS
+ # Schema Info
+ #
+ # Table name: users
+ #
+ # id :integer not null, primary key
+ # firstname :string not null
+ # surname :string not null
+ # value :string not null
+ #
+ # Indexes
+ #
+ # index_rails_02e851e3b7 (id)
+ # index_rails_02e851e3b8 (firstname,surname) WHERE value IS NOT NULL
+ #
+ EOS
+ end
+
+ it 'returns schema info with index information' do
+ is_expected.to eq expected_result
+ end
+ end
+
+ context 'when one of indexes includes "using" clause other than "btree"' do
+ let :columns do
+ [
+ mock_column('id', :integer),
+ mock_column('firstname', :string),
+ mock_column('surname', :string),
+ mock_column('value', :string)
+ ]
+ end
+
+ let :indexes do
+ [
+ mock_index('index_rails_02e851e3b7', columns: ['id']),
+ mock_index('index_rails_02e851e3b8',
+ columns: %w[firstname surname],
+ using: 'hash')
+ ]
+ end
+
+ let :expected_result do
+ <<~EOS
+ # Schema Info
+ #
+ # Table name: users
+ #
+ # id :integer not null, primary key
+ # firstname :string not null
+ # surname :string not null
+ # value :string not null
+ #
+ # Indexes
+ #
+ # index_rails_02e851e3b7 (id)
+ # index_rails_02e851e3b8 (firstname,surname) USING hash
+ #
+ EOS
+ end
+
+ it 'returns schema info with index information' do
+ is_expected.to eq expected_result
+ end
+ end
+
+ context 'when index is not defined' do
+ let :columns do
+ [
+ mock_column(:id, :integer),
+ mock_column(:foreign_thing_id, :integer)
+ ]
+ end
+
+ let :indexes do
+ []
+ end
+
+ let :expected_result do
+ <<~EOS
+ # Schema Info
+ #
+ # Table name: users
+ #
+ # id :integer not null, primary key
+ # foreign_thing_id :integer not null
+ #
+ EOS
+ end
+
+ it 'returns schema info without index information' do
+ is_expected.to eq expected_result
+ end
+ end
+ end
+
+ context 'when option "simple_indexes" is true' do
+ let :options do
+ { simple_indexes: true }
+ end
+
+ context 'when one of indexes includes "orders" clause' do
+ let :columns do
+ [
+ mock_column(:id, :integer),
+ mock_column(:foreign_thing_id, :integer)
+ ]
+ end
+
+ let :indexes do
+ [
+ mock_index('index_rails_02e851e3b7', columns: ['id']),
+ mock_index('index_rails_02e851e3b8',
+ columns: ['foreign_thing_id'],
+ orders: { 'foreign_thing_id' => :desc })
+ ]
+ end
+
+ let :expected_result do
+ <<~EOS
+ # Schema Info
+ #
+ # Table name: users
+ #
+ # id :integer not null, primary key
+ # foreign_thing_id :integer not null
+ #
+ EOS
+ end
+
+ it 'returns schema info with index information' do
+ is_expected.to eq expected_result
+ end
+ end
+
+ context 'when one of indexes is in string form' do
+ let :columns do
+ [
+ mock_column('id', :integer),
+ mock_column('name', :string)
+ ]
+ end
+
+ let :indexes do
+ [
+ mock_index('index_rails_02e851e3b7', columns: ['id']),
+ mock_index('index_rails_02e851e3b8', columns: 'LOWER(name)')
+ ]
+ end
+
+ let :expected_result do
+ <<~EOS
+ # Schema Info
+ #
+ # Table name: users
+ #
+ # id :integer not null, primary key, indexed
+ # name :string not null
+ #
+ EOS
+ end
+
+ it 'returns schema info with index information' do
+ is_expected.to eq expected_result
+ end
+ end
+ end
+ end
+
+ context 'when foreign keys exist' do
+ let :columns do
+ [
+ mock_column(:id, :integer),
+ mock_column(:foreign_thing_id, :integer)
+ ]
+ end
+
+ let :foreign_keys do
+ [
+ mock_foreign_key('fk_rails_cf2568e89e', 'foreign_thing_id', 'foreign_things'),
+ mock_foreign_key('custom_fk_name', 'other_thing_id', 'other_things'),
+ mock_foreign_key('fk_rails_a70234b26c', 'third_thing_id', 'third_things')
+ ]
+ end
+
+ context 'when option "show_foreign_keys" is specified' do
+ let :options do
+ { show_foreign_keys: true }
+ end
+
+ context 'when foreign_keys does not have option' do
+ let :expected_result do
+ <<~EOS
+ # Schema Info
+ #
+ # Table name: users
+ #
+ # id :integer not null, primary key
+ # foreign_thing_id :integer not null
+ #
+ # Foreign Keys
+ #
+ # custom_fk_name (other_thing_id => other_things.id)
+ # fk_rails_... (foreign_thing_id => foreign_things.id)
+ # fk_rails_... (third_thing_id => third_things.id)
+ #
+ EOS
+ end
+
+ it 'returns schema info with foreign keys' do
+ is_expected.to eq(expected_result)
+ end
+ end
+
+ context 'when foreign_keys have option "on_delete" and "on_update"' do
+ let :foreign_keys do
+ [
+ mock_foreign_key('fk_rails_02e851e3b7',
+ 'foreign_thing_id',
+ 'foreign_things',
+ 'id',
+ on_delete: 'on_delete_value',
+ on_update: 'on_update_value')
+ ]
+ end
+
+ let :expected_result do
+ <<~EOS
+ # Schema Info
+ #
+ # Table name: users
+ #
+ # id :integer not null, primary key
+ # foreign_thing_id :integer not null
+ #
+ # Foreign Keys
+ #
+ # fk_rails_... (foreign_thing_id => foreign_things.id) ON DELETE => on_delete_value ON UPDATE => on_update_value
+ #
+ EOS
+ end
+
+ it 'returns schema info with foreign keys' do
+ is_expected.to eq(expected_result)
+ end
+ end
+ end
+
+ context 'when option "show_foreign_keys" and "show_complete_foreign_keys" are specified' do
+ let :options do
+ { show_foreign_keys: true, show_complete_foreign_keys: true }
+ end
+
+ let :expected_result do
+ <<~EOS
+ # Schema Info
+ #
+ # Table name: users
+ #
+ # id :integer not null, primary key
+ # foreign_thing_id :integer not null
+ #
+ # Foreign Keys
+ #
+ # custom_fk_name (other_thing_id => other_things.id)
+ # fk_rails_a70234b26c (third_thing_id => third_things.id)
+ # fk_rails_cf2568e89e (foreign_thing_id => foreign_things.id)
+ #
+ EOS
+ end
+
+ it 'returns schema info with foreign keys' do
+ is_expected.to eq(expected_result)
+ end
+ end
+ end
+
+ context 'when "hide_limit_column_types" is specified in options' do
+ let :columns do
+ [
+ mock_column(:id, :integer, limit: 8),
+ mock_column(:active, :boolean, limit: 1),
+ mock_column(:name, :string, limit: 50),
+ mock_column(:notes, :text, limit: 55)
+ ]
+ end
+
+ context 'when "hide_limit_column_types" is blank string' do
+ let :options do
+ { hide_limit_column_types: '' }
+ end
+
+ let :expected_result do
+ <<~EOS
+ # Schema Info
+ #
+ # Table name: users
+ #
+ # id :integer not null, primary key
+ # active :boolean not null
+ # name :string(50) not null
+ # notes :text(55) not null
+ #
+ EOS
+ end
+
+ it 'works with option "hide_limit_column_types"' do
+ is_expected.to eq expected_result
+ end
+ end
+
+ context 'when "hide_limit_column_types" is "integer,boolean"' do
+ let :options do
+ { hide_limit_column_types: 'integer,boolean' }
+ end
+
+ let :expected_result do
+ <<~EOS
+ # Schema Info
+ #
+ # Table name: users
+ #
+ # id :integer not null, primary key
+ # active :boolean not null
+ # name :string(50) not null
+ # notes :text(55) not null
+ #
+ EOS
+ end
+
+ it 'works with option "hide_limit_column_types"' do
+ is_expected.to eq expected_result
+ end
+ end
+
+ context 'when "hide_limit_column_types" is "integer,boolean,string,text"' do
+ let :options do
+ { hide_limit_column_types: 'integer,boolean,string,text' }
+ end
+
+ let :expected_result do
+ <<~EOS
+ # Schema Info
+ #
+ # Table name: users
+ #
+ # id :integer not null, primary key
+ # active :boolean not null
+ # name :string not null
+ # notes :text not null
+ #
+ EOS
+ end
+
+ it 'works with option "hide_limit_column_types"' do
+ is_expected.to eq expected_result
+ end
+ end
+ end
+
+ context 'when "hide_default_column_types" is specified in options' do
+ let :columns do
+ [
+ mock_column(:profile, :json, default: {}),
+ mock_column(:settings, :jsonb, default: {}),
+ mock_column(:parameters, :hstore, default: {})
+ ]
+ end
+
+ context 'when "hide_default_column_types" is blank string' do
+ let :options do
+ { hide_default_column_types: '' }
+ end
+
+ let :expected_result do
+ <<~EOS
+ # Schema Info
+ #
+ # Table name: users
+ #
+ # profile :json not null
+ # settings :jsonb not null
+ # parameters :hstore not null
+ #
+ EOS
+ end
+
+ it 'works with option "hide_default_column_types"' do
+ is_expected.to eq expected_result
+ end
+ end
+
+ context 'when "hide_default_column_types" is "skip"' do
+ let :options do
+ { hide_default_column_types: 'skip' }
+ end
+
+ let :expected_result do
+ <<~EOS
+ # Schema Info
+ #
+ # Table name: users
+ #
+ # profile :json default({}), not null
+ # settings :jsonb default({}), not null
+ # parameters :hstore default({}), not null
+ #
+ EOS
+ end
+
+ it 'works with option "hide_default_column_types"' do
+ is_expected.to eq expected_result
+ end
+ end
+
+ context 'when "hide_default_column_types" is "json"' do
+ let :options do
+ { hide_default_column_types: 'json' }
+ end
+
+ let :expected_result do
+ <<~EOS
+ # Schema Info
+ #
+ # Table name: users
+ #
+ # profile :json not null
+ # settings :jsonb default({}), not null
+ # parameters :hstore default({}), not null
+ #
+ EOS
+ end
+
+ it 'works with option "hide_limit_column_types"' do
+ is_expected.to eq expected_result
+ end
+ end
+ end
+
+ context 'when "classified_sort" is specified in options' do
+ let :columns do
+ [
+ mock_column(:active, :boolean, limit: 1),
+ mock_column(:name, :string, limit: 50),
+ mock_column(:notes, :text, limit: 55)
+ ]
+ end
+
+ context 'when "classified_sort" is "yes"' do
+ let :options do
+ { classified_sort: 'yes' }
+ end
+
+ let :expected_result do
+ <<~EOS
+ # Schema Info
+ #
+ # Table name: users
+ #
+ # active :boolean not null
+ # name :string(50) not null
+ # notes :text(55) not null
+ #
+ EOS
+ end
+
+ it 'works with option "classified_sort"' do
+ is_expected.to eq expected_result
+ end
+ end
+ end
+
+ context 'when "with_comment" is specified in options' do
+ context 'when "with_comment" is "yes"' do
+ let :options do
+ { with_comment: 'yes' }
+ end
+
+ context 'when columns have comments' do
+ let :columns do
+ [
+ mock_column(:id, :integer, limit: 8, comment: 'ID'),
+ mock_column(:active, :boolean, limit: 1, comment: 'Active'),
+ mock_column(:name, :string, limit: 50, comment: 'Name'),
+ mock_column(:notes, :text, limit: 55, comment: 'Notes'),
+ mock_column(:no_comment, :text, limit: 20, comment: nil)
+ ]
+ end
+
+ let :expected_result do
+ <<~EOS
+ # Schema Info
+ #
+ # Table name: users
+ #
+ # id(ID) :integer not null, primary key
+ # active(Active) :boolean not null
+ # name(Name) :string(50) not null
+ # notes(Notes) :text(55) not null
+ # no_comment :text(20) not null
+ #
+ EOS
+ end
+
+ it 'works with option "with_comment"' do
+ is_expected.to eq expected_result
+ end
+ end
+
+ context 'when columns have multibyte comments' do
+ let :columns do
+ [
+ mock_column(:id, :integer, limit: 8, comment: 'ID'),
+ mock_column(:active, :boolean, limit: 1, comment: 'ACTIVE'),
+ mock_column(:name, :string, limit: 50, comment: 'NAME'),
+ mock_column(:notes, :text, limit: 55, comment: 'NOTES'),
+ mock_column(:cyrillic, :text, limit: 30, comment: 'Кириллица'),
+ mock_column(:japanese, :text, limit: 60, comment: '熊本大学 イタリア 宝島'),
+ mock_column(:arabic, :text, limit: 20, comment: 'لغة'),
+ mock_column(:no_comment, :text, limit: 20, comment: nil),
+ mock_column(:location, :geometry_collection, limit: nil, comment: nil)
+ ]
+ end
+
+ let :expected_result do
+ <<~EOS
+ # Schema Info
+ #
+ # Table name: users
+ #
+ # id(ID) :integer not null, primary key
+ # active(ACTIVE) :boolean not null
+ # name(NAME) :string(50) not null
+ # notes(NOTES) :text(55) not null
+ # cyrillic(Кириллица) :text(30) not null
+ # japanese(熊本大学 イタリア 宝島) :text(60) not null
+ # arabic(لغة) :text(20) not null
+ # no_comment :text(20) not null
+ # location :geometry_collect not null
+ #
+ EOS
+ end
+
+ it 'works with option "with_comment"' do
+ is_expected.to eq expected_result
+ end
+ end
+
+ context 'when columns have multiline comments' do
+ let :columns do
+ [
+ mock_column(:id, :integer, limit: 8, comment: 'ID'),
+ mock_column(:notes, :text, limit: 55, comment: "Notes.\nMay include things like notes."),
+ mock_column(:no_comment, :text, limit: 20, comment: nil)
+ ]
+ end
+
+ let :expected_result do
+ <<~EOS
+ # Schema Info
+ #
+ # Table name: users
+ #
+ # id(ID) :integer not null, primary key
+ # notes(Notes.\\nMay include things like notes.):text(55) not null
+ # no_comment :text(20) not null
+ #
+ EOS
+ end
+
+ it 'works with option "with_comment"' do
+ is_expected.to eq expected_result
+ end
+ end
+
+ context 'when geometry columns are included' do
+ let :columns do
+ [
+ mock_column(:id, :integer, limit: 8),
+ mock_column(:active, :boolean, default: false, null: false),
+ mock_column(:geometry, :geometry,
+ geometric_type: 'Geometry', srid: 4326,
+ limit: { srid: 4326, type: 'geometry' }),
+ mock_column(:location, :geography,
+ geometric_type: 'Point', srid: 0,
+ limit: { srid: 0, type: 'geometry' })
+ ]
+ end
+
+ let :expected_result do
+ <<~EOS
+ # Schema Info
+ #
+ # Table name: users
+ #
+ # id :integer not null, primary key
+ # active :boolean default(FALSE), not null
+ # geometry :geometry not null, geometry, 4326
+ # location :geography not null, point, 0
+ #
+ EOS
+ end
+
+ it 'works with option "with_comment"' do
+ is_expected.to eq expected_result
+ end
+ end
+ end
+ end
+ end
+ end
+ end
+
+ context 'when header is "== Schema Information"' do
+ let :header do
+ AnnotateModels::PREFIX
+ end
+
+ context 'when the primary key is specified' do
+ context 'when the primary_key is :id' do
+ let :primary_key do
+ :id
+ end
+
+ let :columns do
+ [
+ mock_column(:id, :integer),
+ mock_column(:name, :string, limit: 50)
+ ]
+ end
+
+ context 'when option "format_rdoc" is true' do
+ let :options do
+ { format_rdoc: true }
+ end
+
+ let :expected_result do
+ <<~EOS
+ # == Schema Information
+ #
+ # Table name: users
+ #
+ # *id*:: integer, not null, primary key
+ # *name*:: string(50), not null
+ #--
+ # == Schema Information End
+ #++
+ EOS
+ end
+
+ it 'returns schema info in RDoc format' do
+ is_expected.to eq(expected_result)
+ end
+ end
+
+ context 'when option "format_yard" is true' do
+ let :options do
+ { format_yard: true }
+ end
+
+ let :expected_result do
+ <<~EOS
+ # == Schema Information
+ #
+ # Table name: users
+ #
+ # @!attribute id
+ # @return [Integer]
+ # @!attribute name
+ # @return [String]
+ #
+ EOS
+ end
+
+ it 'returns schema info in YARD format' do
+ is_expected.to eq(expected_result)
+ end
+ end
+
+ context 'when option "format_markdown" is true' do
+ context 'when other option is not specified' do
+ let :options do
+ { format_markdown: true }
+ end
+
+ let :expected_result do
+ <<~EOS
+ # == Schema Information
+ #
+ # Table name: `users`
+ #
+ # ### Columns
+ #
+ # Name | Type | Attributes
+ # ----------- | ------------------ | ---------------------------
+ # **`id`** | `integer` | `not null, primary key`
+ # **`name`** | `string(50)` | `not null`
+ #
+ EOS
+ end
+
+ it 'returns schema info in Markdown format' do
+ is_expected.to eq(expected_result)
+ end
+ end
+
+ context 'when option "show_indexes" is true' do
+ let :options do
+ { format_markdown: true, show_indexes: true }
+ end
+
+ context 'when indexes are normal' do
+ let :indexes do
+ [
+ mock_index('index_rails_02e851e3b7', columns: ['id']),
+ mock_index('index_rails_02e851e3b8', columns: ['foreign_thing_id'])
+ ]
+ end
+
+ let :expected_result do
+ <<~EOS
+ # == Schema Information
+ #
+ # Table name: `users`
+ #
+ # ### Columns
+ #
+ # Name | Type | Attributes
+ # ----------- | ------------------ | ---------------------------
+ # **`id`** | `integer` | `not null, primary key`
+ # **`name`** | `string(50)` | `not null`
+ #
+ # ### Indexes
+ #
+ # * `index_rails_02e851e3b7`:
+ # * **`id`**
+ # * `index_rails_02e851e3b8`:
+ # * **`foreign_thing_id`**
+ #
+ EOS
+ end
+
+ it 'returns schema info with index information in Markdown format' do
+ is_expected.to eq expected_result
+ end
+ end
+
+ context 'when one of indexes includes "unique" clause' do
+ let :indexes do
+ [
+ mock_index('index_rails_02e851e3b7', columns: ['id']),
+ mock_index('index_rails_02e851e3b8',
+ columns: ['foreign_thing_id'],
+ unique: true)
+ ]
+ end
+
+ let :expected_result do
+ <<~EOS
+ # == Schema Information
+ #
+ # Table name: `users`
+ #
+ # ### Columns
+ #
+ # Name | Type | Attributes
+ # ----------- | ------------------ | ---------------------------
+ # **`id`** | `integer` | `not null, primary key`
+ # **`name`** | `string(50)` | `not null`
+ #
+ # ### Indexes
+ #
+ # * `index_rails_02e851e3b7`:
+ # * **`id`**
+ # * `index_rails_02e851e3b8` (_unique_):
+ # * **`foreign_thing_id`**
+ #
+ EOS
+ end
+
+ it 'returns schema info with index information in Markdown format' do
+ is_expected.to eq expected_result
+ end
+ end
+
+ context 'when one of indexes includes orderd index key' do
+ let :indexes do
+ [
+ mock_index('index_rails_02e851e3b7', columns: ['id']),
+ mock_index('index_rails_02e851e3b8',
+ columns: ['foreign_thing_id'],
+ orders: { 'foreign_thing_id' => :desc })
+ ]
+ end
+
+ let :expected_result do
+ <<~EOS
+ # == Schema Information
+ #
+ # Table name: `users`
+ #
+ # ### Columns
+ #
+ # Name | Type | Attributes
+ # ----------- | ------------------ | ---------------------------
+ # **`id`** | `integer` | `not null, primary key`
+ # **`name`** | `string(50)` | `not null`
+ #
+ # ### Indexes
+ #
+ # * `index_rails_02e851e3b7`:
+ # * **`id`**
+ # * `index_rails_02e851e3b8`:
+ # * **`foreign_thing_id DESC`**
+ #
+ EOS
+ end
+
+ it 'returns schema info with index information in Markdown format' do
+ is_expected.to eq expected_result
+ end
+ end
+
+ context 'when one of indexes includes "where" clause and "unique" clause' do
+ let :indexes do
+ [
+ mock_index('index_rails_02e851e3b7', columns: ['id']),
+ mock_index('index_rails_02e851e3b8',
+ columns: ['foreign_thing_id'],
+ unique: true,
+ where: 'name IS NOT NULL')
+ ]
+ end
+
+ let :expected_result do
+ <<~EOS
+ # == Schema Information
+ #
+ # Table name: `users`
+ #
+ # ### Columns
+ #
+ # Name | Type | Attributes
+ # ----------- | ------------------ | ---------------------------
+ # **`id`** | `integer` | `not null, primary key`
+ # **`name`** | `string(50)` | `not null`
+ #
+ # ### Indexes
+ #
+ # * `index_rails_02e851e3b7`:
+ # * **`id`**
+ # * `index_rails_02e851e3b8` (_unique_ _where_ name IS NOT NULL):
+ # * **`foreign_thing_id`**
+ #
+ EOS
+ end
+
+ it 'returns schema info with index information in Markdown format' do
+ is_expected.to eq expected_result
+ end
+ end
+
+ context 'when one of indexes includes "using" clause other than "btree"' do
+ let :indexes do
+ [
+ mock_index('index_rails_02e851e3b7', columns: ['id']),
+ mock_index('index_rails_02e851e3b8',
+ columns: ['foreign_thing_id'],
+ using: 'hash')
+ ]
+ end
+
+ let :expected_result do
+ <<~EOS
+ # == Schema Information
+ #
+ # Table name: `users`
+ #
+ # ### Columns
+ #
+ # Name | Type | Attributes
+ # ----------- | ------------------ | ---------------------------
+ # **`id`** | `integer` | `not null, primary key`
+ # **`name`** | `string(50)` | `not null`
+ #
+ # ### Indexes
+ #
+ # * `index_rails_02e851e3b7`:
+ # * **`id`**
+ # * `index_rails_02e851e3b8` (_using_ hash):
+ # * **`foreign_thing_id`**
+ #
+ EOS
+ end
+
+ it 'returns schema info with index information in Markdown format' do
+ is_expected.to eq expected_result
+ end
+ end
+ end
+
+ context 'when option "show_foreign_keys" is true' do
+ let :options do
+ { format_markdown: true, show_foreign_keys: true }
+ end
+
+ let :columns do
+ [
+ mock_column(:id, :integer),
+ mock_column(:foreign_thing_id, :integer)
+ ]
+ end
+
+ context 'when foreign_keys have option "on_delete" and "on_update"' do
+ let :foreign_keys do
+ [
+ mock_foreign_key('fk_rails_02e851e3b7',
+ 'foreign_thing_id',
+ 'foreign_things',
+ 'id',
+ on_delete: 'on_delete_value',
+ on_update: 'on_update_value')
+ ]
+ end
+
+ let :expected_result do
+ <<~EOS
+ # == Schema Information
+ #
+ # Table name: `users`
+ #
+ # ### Columns
+ #
+ # Name | Type | Attributes
+ # ----------------------- | ------------------ | ---------------------------
+ # **`id`** | `integer` | `not null, primary key`
+ # **`foreign_thing_id`** | `integer` | `not null`
+ #
+ # ### Foreign Keys
+ #
+ # * `fk_rails_...` (_ON DELETE => on_delete_value ON UPDATE => on_update_value_):
+ # * **`foreign_thing_id => foreign_things.id`**
+ #
+ EOS
+ end
+
+ it 'returns schema info with foreign_keys in Markdown format' do
+ is_expected.to eq(expected_result)
+ end
+ end
+ end
+ end
+
+ context 'when "format_doc" and "with_comment" are specified in options' do
+ let :options do
+ { format_rdoc: true, with_comment: true }
+ end
+
+ context 'when columns are normal' do
+ let :columns do
+ [
+ mock_column(:id, :integer, comment: 'ID'),
+ mock_column(:name, :string, limit: 50, comment: 'Name')
+ ]
+ end
+
+ let :expected_result do
+ <<~EOS
+ # == Schema Information
+ #
+ # Table name: users
+ #
+ # *id(ID)*:: integer, not null, primary key
+ # *name(Name)*:: string(50), not null
+ #--
+ # == Schema Information End
+ #++
+ EOS
+ end
+
+ it 'returns schema info in RDoc format' do
+ is_expected.to eq expected_result
+ end
+ end
+ end
+
+ context 'when "format_markdown" and "with_comment" are specified in options' do
+ let :options do
+ { format_markdown: true, with_comment: true }
+ end
+
+ context 'when columns have comments' do
+ let :columns do
+ [
+ mock_column(:id, :integer, comment: 'ID'),
+ mock_column(:name, :string, limit: 50, comment: 'Name')
+ ]
+ end
+
+ let :expected_result do
+ <<~EOS
+ # == Schema Information
+ #
+ # Table name: `users`
+ #
+ # ### Columns
+ #
+ # Name | Type | Attributes
+ # ----------------- | ------------------ | ---------------------------
+ # **`id(ID)`** | `integer` | `not null, primary key`
+ # **`name(Name)`** | `string(50)` | `not null`
+ #
+ EOS
+ end
+
+ it 'returns schema info in Markdown format' do
+ is_expected.to eq expected_result
+ end
+ end
+
+ context 'when columns have multibyte comments' do
+ let :columns do
+ [
+ mock_column(:id, :integer, comment: 'ID'),
+ mock_column(:name, :string, limit: 50, comment: 'NAME')
+ ]
+ end
+
+ let :expected_result do
+ <<~EOS
+ # == Schema Information
+ #
+ # Table name: `users`
+ #
+ # ### Columns
+ #
+ # Name | Type | Attributes
+ # --------------------- | ------------------ | ---------------------------
+ # **`id(ID)`** | `integer` | `not null, primary key`
+ # **`name(NAME)`** | `string(50)` | `not null`
+ #
+ EOS
+ end
+
+ it 'returns schema info in Markdown format' do
+ is_expected.to eq expected_result
+ end
+ end
+ end
+ end
+ end
+ end
+ end
+ end
+
+ describe 'private methods' do
+ describe '.quote' do
+ subject do
+ AnnotateModels::SchemaInfo.send(:quote, value)
+ end
+
+ context 'when the argument is nil' do
+ let(:value) { nil }
+ it 'returns string "NULL"' do
+ is_expected.to eq('NULL')
+ end
+ end
+
+ context 'when the argument is true' do
+ let(:value) { true }
+ it 'returns string "TRUE"' do
+ is_expected.to eq('TRUE')
+ end
+ end
+
+ context 'when the argument is false' do
+ let(:value) { false }
+ it 'returns string "FALSE"' do
+ is_expected.to eq('FALSE')
+ end
+ end
+
+ context 'when the argument is an integer' do
+ let(:value) { 25 }
+ it 'returns the integer as a string' do
+ is_expected.to eq('25')
+ end
+ end
+
+ context 'when the argument is a float number' do
+ context 'when the argument is like 25.6' do
+ let(:value) { 25.6 }
+ it 'returns the float number as a string' do
+ is_expected.to eq('25.6')
+ end
+ end
+
+ context 'when the argument is like 1e-20' do
+ let(:value) { 1e-20 }
+ it 'returns the float number as a string' do
+ is_expected.to eq('1.0e-20')
+ end
+ end
+ end
+
+ context 'when the argument is a BigDecimal number' do
+ let(:value) { BigDecimal('1.2') }
+ it 'returns the float number as a string' do
+ is_expected.to eq('1.2')
+ end
+ end
+
+ context 'when the argument is an array' do
+ let(:value) { [BigDecimal('1.2')] }
+ it 'returns an array of which elements are converted to string' do
+ is_expected.to eq(['1.2'])
+ end
+ end
+ end
+ end
+end
diff --git a/spec/lib/annotate/annotate_models_spec.rb b/spec/lib/annotate/annotate_models_spec.rb
index d9f74cf9d..3c24d16bf 100644
--- a/spec/lib/annotate/annotate_models_spec.rb
+++ b/spec/lib/annotate/annotate_models_spec.rb
@@ -21,128 +21,6 @@
'# -*- frozen_string_literal : true -*-'
].freeze
- def mock_index(name, params = {})
- double('IndexKeyDefinition',
- name: name,
- columns: params[:columns] || [],
- unique: params[:unique] || false,
- orders: params[:orders] || {},
- where: params[:where],
- using: params[:using])
- end
-
- def mock_foreign_key(name, from_column, to_table, to_column = 'id', constraints = {})
- double('ForeignKeyDefinition',
- name: name,
- column: from_column,
- to_table: to_table,
- primary_key: to_column,
- on_delete: constraints[:on_delete],
- on_update: constraints[:on_update])
- end
-
- def mock_connection(indexes = [], foreign_keys = [])
- double('Conn',
- indexes: indexes,
- foreign_keys: foreign_keys,
- supports_foreign_keys?: true)
- end
-
- def mock_class(table_name, primary_key, columns, indexes = [], foreign_keys = [])
- options = {
- connection: mock_connection(indexes, foreign_keys),
- table_exists?: true,
- table_name: table_name,
- primary_key: primary_key,
- column_names: columns.map { |col| col.name.to_s },
- columns: columns,
- column_defaults: Hash[columns.map { |col| [col.name, col.default] }],
- table_name_prefix: ''
- }
-
- double('An ActiveRecord class', options)
- end
-
- def mock_column(name, type, options = {})
- default_options = {
- limit: nil,
- null: false,
- default: nil,
- sql_type: type
- }
-
- stubs = default_options.dup
- stubs.merge!(options)
- stubs[:name] = name
- stubs[:type] = type
-
- double('Column', stubs)
- end
-
- describe '.quote' do
- subject do
- AnnotateModels.quote(value)
- end
-
- context 'when the argument is nil' do
- let(:value) { nil }
- it 'returns string "NULL"' do
- is_expected.to eq('NULL')
- end
- end
-
- context 'when the argument is true' do
- let(:value) { true }
- it 'returns string "TRUE"' do
- is_expected.to eq('TRUE')
- end
- end
-
- context 'when the argument is false' do
- let(:value) { false }
- it 'returns string "FALSE"' do
- is_expected.to eq('FALSE')
- end
- end
-
- context 'when the argument is an integer' do
- let(:value) { 25 }
- it 'returns the integer as a string' do
- is_expected.to eq('25')
- end
- end
-
- context 'when the argument is a float number' do
- context 'when the argument is like 25.6' do
- let(:value) { 25.6 }
- it 'returns the float number as a string' do
- is_expected.to eq('25.6')
- end
- end
-
- context 'when the argument is like 1e-20' do
- let(:value) { 1e-20 }
- it 'returns the float number as a string' do
- is_expected.to eq('1.0e-20')
- end
- end
- end
-
- context 'when the argument is a BigDecimal number' do
- let(:value) { BigDecimal('1.2') }
- it 'returns the float number as a string' do
- is_expected.to eq('1.2')
- end
- end
-
- context 'when the argument is an array' do
- let(:value) { [BigDecimal('1.2')] }
- it 'returns an array of which elements are converted to string' do
- is_expected.to eq(['1.2'])
- end
- end
- end
-
describe '.parse_options' do
let(:options) do
{
@@ -176,1433 +54,6 @@ def mock_column(name, type, options = {})
end
end
- describe '.get_schema_info' do
- subject do
- AnnotateModels.get_schema_info(klass, header, **options)
- end
-
- let :klass do
- mock_class(:users, primary_key, columns, indexes, foreign_keys)
- end
-
- let :indexes do
- []
- end
-
- let :foreign_keys do
- []
- end
-
- context 'when option is not present' do
- let :options do
- {}
- end
-
- context 'when header is "Schema Info"' do
- let :header do
- 'Schema Info'
- end
-
- context 'when the primary key is not specified' do
- let :primary_key do
- nil
- end
-
- context 'when the columns are normal' do
- let :columns do
- [
- mock_column(:id, :integer),
- mock_column(:name, :string, limit: 50)
- ]
- end
-
- let :expected_result do
- <<~EOS
- # Schema Info
- #
- # Table name: users
- #
- # id :integer not null
- # name :string(50) not null
- #
- EOS
- end
-
- it 'returns schema info' do
- is_expected.to eq(expected_result)
- end
- end
-
- context 'when an enum column exists' do
- let :columns do
- [
- mock_column(:id, :integer),
- mock_column(:name, :enum, limit: [:enum1, :enum2])
- ]
- end
-
- let :expected_result do
- <<~EOS
- # Schema Info
- #
- # Table name: users
- #
- # id :integer not null
- # name :enum not null, (enum1, enum2)
- #
- EOS
- end
-
- it 'returns schema info' do
- is_expected.to eq(expected_result)
- end
- end
-
- context 'when unsigned columns exist' do
- let :columns do
- [
- mock_column(:id, :integer),
- mock_column(:integer, :integer, unsigned?: true),
- mock_column(:bigint, :integer, unsigned?: true, bigint?: true),
- mock_column(:bigint, :bigint, unsigned?: true),
- mock_column(:float, :float, unsigned?: true),
- mock_column(:decimal, :decimal, unsigned?: true, precision: 10, scale: 2),
- ]
- end
-
- let :expected_result do
- <<~EOS
- # Schema Info
- #
- # Table name: users
- #
- # id :integer not null
- # integer :integer unsigned, not null
- # bigint :bigint unsigned, not null
- # bigint :bigint unsigned, not null
- # float :float unsigned, not null
- # decimal :decimal(10, 2) unsigned, not null
- #
- EOS
- end
-
- it 'returns schema info' do
- is_expected.to eq(expected_result)
- end
- end
- end
-
- context 'when the primary key is specified' do
- context 'when the primary_key is :id' do
- let :primary_key do
- :id
- end
-
- context 'when columns are normal' do
- let :columns do
- [
- mock_column(:id, :integer, limit: 8),
- mock_column(:name, :string, limit: 50),
- mock_column(:notes, :text, limit: 55)
- ]
- end
-
- let :expected_result do
- <<~EOS
- # Schema Info
- #
- # Table name: users
- #
- # id :integer not null, primary key
- # name :string(50) not null
- # notes :text(55) not null
- #
- EOS
- end
-
- it 'returns schema info' do
- is_expected.to eq(expected_result)
- end
- end
-
- context 'when columns have default values' do
- let :columns do
- [
- mock_column(:id, :integer),
- mock_column(:size, :integer, default: 20),
- mock_column(:flag, :boolean, default: false)
- ]
- end
-
- let :expected_result do
- <<~EOS
- # Schema Info
- #
- # Table name: users
- #
- # id :integer not null, primary key
- # size :integer default(20), not null
- # flag :boolean default(FALSE), not null
- #
- EOS
- end
-
- it 'returns schema info with default values' do
- is_expected.to eq(expected_result)
- end
- end
-
- context 'with Globalize gem' do
- let :translation_klass do
- double('Post::Translation',
- to_s: 'Post::Translation',
- columns: [
- mock_column(:id, :integer, limit: 8),
- mock_column(:post_id, :integer, limit: 8),
- mock_column(:locale, :string, limit: 50),
- mock_column(:title, :string, limit: 50),
- ])
- end
-
- let :klass do
- mock_class(:posts, primary_key, columns, indexes, foreign_keys).tap do |mock_klass|
- allow(mock_klass).to receive(:translation_class).and_return(translation_klass)
- end
- end
-
- let :columns do
- [
- mock_column(:id, :integer, limit: 8),
- mock_column(:author_name, :string, limit: 50),
- ]
- end
-
- let :expected_result do
- <<~EOS
- # Schema Info
- #
- # Table name: posts
- #
- # id :integer not null, primary key
- # author_name :string(50) not null
- # title :string(50) not null
- #
- EOS
- end
-
- it 'returns schema info' do
- is_expected.to eq expected_result
- end
- end
- end
-
- context 'when the primary key is an array (using composite_primary_keys)' do
- let :primary_key do
- [:a_id, :b_id]
- end
-
- let :columns do
- [
- mock_column(:a_id, :integer),
- mock_column(:b_id, :integer),
- mock_column(:name, :string, limit: 50)
- ]
- end
-
- let :expected_result do
- <<~EOS
- # Schema Info
- #
- # Table name: users
- #
- # a_id :integer not null, primary key
- # b_id :integer not null, primary key
- # name :string(50) not null
- #
- EOS
- end
-
- it 'returns schema info' do
- is_expected.to eq(expected_result)
- end
- end
- end
- end
- end
-
- context 'when option is present' do
- context 'when header is "Schema Info"' do
- let :header do
- 'Schema Info'
- end
-
- context 'when the primary key is specified' do
- context 'when the primary_key is :id' do
- let :primary_key do
- :id
- end
-
- context 'when indexes exist' do
- context 'when option "show_indexes" is true' do
- let :options do
- { show_indexes: true }
- end
-
- context 'when indexes are normal' do
- let :columns do
- [
- mock_column(:id, :integer),
- mock_column(:foreign_thing_id, :integer)
- ]
- end
-
- let :indexes do
- [
- mock_index('index_rails_02e851e3b7', columns: ['id']),
- mock_index('index_rails_02e851e3b8', columns: ['foreign_thing_id'])
- ]
- end
-
- let :expected_result do
- <<~EOS
- # Schema Info
- #
- # Table name: users
- #
- # id :integer not null, primary key
- # foreign_thing_id :integer not null
- #
- # Indexes
- #
- # index_rails_02e851e3b7 (id)
- # index_rails_02e851e3b8 (foreign_thing_id)
- #
- EOS
- end
-
- it 'returns schema info with index information' do
- is_expected.to eq expected_result
- end
- end
-
- context 'when one of indexes includes orderd index key' do
- let :columns do
- [
- mock_column("id", :integer),
- mock_column("firstname", :string),
- mock_column("surname", :string),
- mock_column("value", :string)
- ]
- end
-
- let :indexes do
- [
- mock_index('index_rails_02e851e3b7', columns: ['id']),
- mock_index('index_rails_02e851e3b8',
- columns: %w(firstname surname value),
- orders: { 'surname' => :asc, 'value' => :desc })
- ]
- end
-
- let :expected_result do
- <<~EOS
- # Schema Info
- #
- # Table name: users
- #
- # id :integer not null, primary key
- # firstname :string not null
- # surname :string not null
- # value :string not null
- #
- # Indexes
- #
- # index_rails_02e851e3b7 (id)
- # index_rails_02e851e3b8 (firstname,surname ASC,value DESC)
- #
- EOS
- end
-
- it 'returns schema info with index information' do
- is_expected.to eq expected_result
- end
- end
-
- context 'when one of indexes includes "where" clause' do
- let :columns do
- [
- mock_column("id", :integer),
- mock_column("firstname", :string),
- mock_column("surname", :string),
- mock_column("value", :string)
- ]
- end
-
- let :indexes do
- [
- mock_index('index_rails_02e851e3b7', columns: ['id']),
- mock_index('index_rails_02e851e3b8',
- columns: %w(firstname surname),
- where: 'value IS NOT NULL')
- ]
- end
-
- let :expected_result do
- <<~EOS
- # Schema Info
- #
- # Table name: users
- #
- # id :integer not null, primary key
- # firstname :string not null
- # surname :string not null
- # value :string not null
- #
- # Indexes
- #
- # index_rails_02e851e3b7 (id)
- # index_rails_02e851e3b8 (firstname,surname) WHERE value IS NOT NULL
- #
- EOS
- end
-
- it 'returns schema info with index information' do
- is_expected.to eq expected_result
- end
- end
-
- context 'when one of indexes includes "using" clause other than "btree"' do
- let :columns do
- [
- mock_column("id", :integer),
- mock_column("firstname", :string),
- mock_column("surname", :string),
- mock_column("value", :string)
- ]
- end
-
- let :indexes do
- [
- mock_index('index_rails_02e851e3b7', columns: ['id']),
- mock_index('index_rails_02e851e3b8',
- columns: %w(firstname surname),
- using: 'hash')
- ]
- end
-
- let :expected_result do
- <<~EOS
- # Schema Info
- #
- # Table name: users
- #
- # id :integer not null, primary key
- # firstname :string not null
- # surname :string not null
- # value :string not null
- #
- # Indexes
- #
- # index_rails_02e851e3b7 (id)
- # index_rails_02e851e3b8 (firstname,surname) USING hash
- #
- EOS
- end
-
- it 'returns schema info with index information' do
- is_expected.to eq expected_result
- end
- end
-
- context 'when index is not defined' do
- let :columns do
- [
- mock_column(:id, :integer),
- mock_column(:foreign_thing_id, :integer)
- ]
- end
-
- let :indexes do
- []
- end
-
- let :expected_result do
- <<~EOS
- # Schema Info
- #
- # Table name: users
- #
- # id :integer not null, primary key
- # foreign_thing_id :integer not null
- #
- EOS
- end
-
- it 'returns schema info without index information' do
- is_expected.to eq expected_result
- end
- end
- end
-
- context 'when option "simple_indexes" is true' do
- let :options do
- { simple_indexes: true }
- end
-
- context 'when one of indexes includes "orders" clause' do
- let :columns do
- [
- mock_column(:id, :integer),
- mock_column(:foreign_thing_id, :integer)
- ]
- end
-
- let :indexes do
- [
- mock_index('index_rails_02e851e3b7', columns: ['id']),
- mock_index('index_rails_02e851e3b8',
- columns: ['foreign_thing_id'],
- orders: { 'foreign_thing_id' => :desc })
- ]
- end
-
- let :expected_result do
- <<~EOS
- # Schema Info
- #
- # Table name: users
- #
- # id :integer not null, primary key
- # foreign_thing_id :integer not null
- #
- EOS
- end
-
- it 'returns schema info with index information' do
- is_expected.to eq expected_result
- end
- end
-
- context 'when one of indexes is in string form' do
- let :columns do
- [
- mock_column("id", :integer),
- mock_column("name", :string)
- ]
- end
-
- let :indexes do
- [
- mock_index('index_rails_02e851e3b7', columns: ['id']),
- mock_index('index_rails_02e851e3b8', columns: 'LOWER(name)')
- ]
- end
-
- let :expected_result do
- <<~EOS
- # Schema Info
- #
- # Table name: users
- #
- # id :integer not null, primary key, indexed
- # name :string not null
- #
- EOS
- end
-
- it 'returns schema info with index information' do
- is_expected.to eq expected_result
- end
- end
- end
- end
-
- context 'when foreign keys exist' do
- let :columns do
- [
- mock_column(:id, :integer),
- mock_column(:foreign_thing_id, :integer)
- ]
- end
-
- let :foreign_keys do
- [
- mock_foreign_key('fk_rails_cf2568e89e', 'foreign_thing_id', 'foreign_things'),
- mock_foreign_key('custom_fk_name', 'other_thing_id', 'other_things'),
- mock_foreign_key('fk_rails_a70234b26c', 'third_thing_id', 'third_things')
- ]
- end
-
- context 'when option "show_foreign_keys" is specified' do
- let :options do
- { show_foreign_keys: true }
- end
-
- context 'when foreign_keys does not have option' do
- let :expected_result do
- <<~EOS
- # Schema Info
- #
- # Table name: users
- #
- # id :integer not null, primary key
- # foreign_thing_id :integer not null
- #
- # Foreign Keys
- #
- # custom_fk_name (other_thing_id => other_things.id)
- # fk_rails_... (foreign_thing_id => foreign_things.id)
- # fk_rails_... (third_thing_id => third_things.id)
- #
- EOS
- end
-
- it 'returns schema info with foreign keys' do
- is_expected.to eq(expected_result)
- end
- end
-
- context 'when foreign_keys have option "on_delete" and "on_update"' do
- let :foreign_keys do
- [
- mock_foreign_key('fk_rails_02e851e3b7',
- 'foreign_thing_id',
- 'foreign_things',
- 'id',
- on_delete: 'on_delete_value',
- on_update: 'on_update_value')
- ]
- end
-
- let :expected_result do
- <<~EOS
- # Schema Info
- #
- # Table name: users
- #
- # id :integer not null, primary key
- # foreign_thing_id :integer not null
- #
- # Foreign Keys
- #
- # fk_rails_... (foreign_thing_id => foreign_things.id) ON DELETE => on_delete_value ON UPDATE => on_update_value
- #
- EOS
- end
-
- it 'returns schema info with foreign keys' do
- is_expected.to eq(expected_result)
- end
- end
- end
-
- context 'when option "show_foreign_keys" and "show_complete_foreign_keys" are specified' do
- let :options do
- { show_foreign_keys: true, show_complete_foreign_keys: true }
- end
-
- let :expected_result do
- <<~EOS
- # Schema Info
- #
- # Table name: users
- #
- # id :integer not null, primary key
- # foreign_thing_id :integer not null
- #
- # Foreign Keys
- #
- # custom_fk_name (other_thing_id => other_things.id)
- # fk_rails_a70234b26c (third_thing_id => third_things.id)
- # fk_rails_cf2568e89e (foreign_thing_id => foreign_things.id)
- #
- EOS
- end
-
- it 'returns schema info with foreign keys' do
- is_expected.to eq(expected_result)
- end
- end
- end
-
- context 'when "hide_limit_column_types" is specified in options' do
- let :columns do
- [
- mock_column(:id, :integer, limit: 8),
- mock_column(:active, :boolean, limit: 1),
- mock_column(:name, :string, limit: 50),
- mock_column(:notes, :text, limit: 55)
- ]
- end
-
- context 'when "hide_limit_column_types" is blank string' do
- let :options do
- { hide_limit_column_types: '' }
- end
-
- let :expected_result do
- <<~EOS
- # Schema Info
- #
- # Table name: users
- #
- # id :integer not null, primary key
- # active :boolean not null
- # name :string(50) not null
- # notes :text(55) not null
- #
- EOS
- end
-
- it 'works with option "hide_limit_column_types"' do
- is_expected.to eq expected_result
- end
- end
-
- context 'when "hide_limit_column_types" is "integer,boolean"' do
- let :options do
- { hide_limit_column_types: 'integer,boolean' }
- end
-
- let :expected_result do
- <<~EOS
- # Schema Info
- #
- # Table name: users
- #
- # id :integer not null, primary key
- # active :boolean not null
- # name :string(50) not null
- # notes :text(55) not null
- #
- EOS
- end
-
- it 'works with option "hide_limit_column_types"' do
- is_expected.to eq expected_result
- end
- end
-
- context 'when "hide_limit_column_types" is "integer,boolean,string,text"' do
- let :options do
- { hide_limit_column_types: 'integer,boolean,string,text' }
- end
-
- let :expected_result do
- <<~EOS
- # Schema Info
- #
- # Table name: users
- #
- # id :integer not null, primary key
- # active :boolean not null
- # name :string not null
- # notes :text not null
- #
- EOS
- end
-
- it 'works with option "hide_limit_column_types"' do
- is_expected.to eq expected_result
- end
- end
- end
-
- context 'when "hide_default_column_types" is specified in options' do
- let :columns do
- [
- mock_column(:profile, :json, default: {}),
- mock_column(:settings, :jsonb, default: {}),
- mock_column(:parameters, :hstore, default: {})
- ]
- end
-
- context 'when "hide_default_column_types" is blank string' do
- let :options do
- { hide_default_column_types: '' }
- end
-
- let :expected_result do
- <<~EOS
- # Schema Info
- #
- # Table name: users
- #
- # profile :json not null
- # settings :jsonb not null
- # parameters :hstore not null
- #
- EOS
- end
-
- it 'works with option "hide_default_column_types"' do
- is_expected.to eq expected_result
- end
- end
-
- context 'when "hide_default_column_types" is "skip"' do
- let :options do
- { hide_default_column_types: 'skip' }
- end
-
- let :expected_result do
- <<~EOS
- # Schema Info
- #
- # Table name: users
- #
- # profile :json default({}), not null
- # settings :jsonb default({}), not null
- # parameters :hstore default({}), not null
- #
- EOS
- end
-
- it 'works with option "hide_default_column_types"' do
- is_expected.to eq expected_result
- end
- end
-
- context 'when "hide_default_column_types" is "json"' do
- let :options do
- { hide_default_column_types: 'json' }
- end
-
- let :expected_result do
- <<~EOS
- # Schema Info
- #
- # Table name: users
- #
- # profile :json not null
- # settings :jsonb default({}), not null
- # parameters :hstore default({}), not null
- #
- EOS
- end
-
- it 'works with option "hide_limit_column_types"' do
- is_expected.to eq expected_result
- end
- end
- end
-
- context 'when "classified_sort" is specified in options' do
- let :columns do
- [
- mock_column(:active, :boolean, limit: 1),
- mock_column(:name, :string, limit: 50),
- mock_column(:notes, :text, limit: 55)
- ]
- end
-
- context 'when "classified_sort" is "yes"' do
- let :options do
- { classified_sort: 'yes' }
- end
-
- let :expected_result do
- <<~EOS
- # Schema Info
- #
- # Table name: users
- #
- # active :boolean not null
- # name :string(50) not null
- # notes :text(55) not null
- #
- EOS
- end
-
- it 'works with option "classified_sort"' do
- is_expected.to eq expected_result
- end
- end
- end
-
- context 'when "with_comment" is specified in options' do
- context 'when "with_comment" is "yes"' do
- let :options do
- { with_comment: 'yes' }
- end
-
- context 'when columns have comments' do
- let :columns do
- [
- mock_column(:id, :integer, limit: 8, comment: 'ID'),
- mock_column(:active, :boolean, limit: 1, comment: 'Active'),
- mock_column(:name, :string, limit: 50, comment: 'Name'),
- mock_column(:notes, :text, limit: 55, comment: 'Notes'),
- mock_column(:no_comment, :text, limit: 20, comment: nil)
- ]
- end
-
- let :expected_result do
- <<~EOS
- # Schema Info
- #
- # Table name: users
- #
- # id(ID) :integer not null, primary key
- # active(Active) :boolean not null
- # name(Name) :string(50) not null
- # notes(Notes) :text(55) not null
- # no_comment :text(20) not null
- #
- EOS
- end
-
- it 'works with option "with_comment"' do
- is_expected.to eq expected_result
- end
- end
-
- context 'when columns have multibyte comments' do
- let :columns do
- [
- mock_column(:id, :integer, limit: 8, comment: 'ID'),
- mock_column(:active, :boolean, limit: 1, comment: 'ACTIVE'),
- mock_column(:name, :string, limit: 50, comment: 'NAME'),
- mock_column(:notes, :text, limit: 55, comment: 'NOTES'),
- mock_column(:cyrillic, :text, limit: 30, comment: 'Кириллица'),
- mock_column(:japanese, :text, limit: 60, comment: '熊本大学 イタリア 宝島'),
- mock_column(:arabic, :text, limit: 20, comment: 'لغة'),
- mock_column(:no_comment, :text, limit: 20, comment: nil),
- mock_column(:location, :geometry_collection, limit: nil, comment: nil)
- ]
- end
-
- let :expected_result do
- <<~EOS
- # Schema Info
- #
- # Table name: users
- #
- # id(ID) :integer not null, primary key
- # active(ACTIVE) :boolean not null
- # name(NAME) :string(50) not null
- # notes(NOTES) :text(55) not null
- # cyrillic(Кириллица) :text(30) not null
- # japanese(熊本大学 イタリア 宝島) :text(60) not null
- # arabic(لغة) :text(20) not null
- # no_comment :text(20) not null
- # location :geometry_collect not null
- #
- EOS
- end
-
- it 'works with option "with_comment"' do
- is_expected.to eq expected_result
- end
- end
-
- context 'when columns have multiline comments' do
- let :columns do
- [
- mock_column(:id, :integer, limit: 8, comment: 'ID'),
- mock_column(:notes, :text, limit: 55, comment: "Notes.\nMay include things like notes."),
- mock_column(:no_comment, :text, limit: 20, comment: nil)
- ]
- end
-
- let :expected_result do
- <<~EOS
- # Schema Info
- #
- # Table name: users
- #
- # id(ID) :integer not null, primary key
- # notes(Notes.\\nMay include things like notes.):text(55) not null
- # no_comment :text(20) not null
- #
- EOS
- end
-
- it 'works with option "with_comment"' do
- is_expected.to eq expected_result
- end
- end
-
- context 'when geometry columns are included' do
- let :columns do
- [
- mock_column(:id, :integer, limit: 8),
- mock_column(:active, :boolean, default: false, null: false),
- mock_column(:geometry, :geometry,
- geometric_type: 'Geometry', srid: 4326,
- limit: { srid: 4326, type: 'geometry' }),
- mock_column(:location, :geography,
- geometric_type: 'Point', srid: 0,
- limit: { srid: 0, type: 'geometry' })
- ]
- end
-
- let :expected_result do
- <<~EOS
- # Schema Info
- #
- # Table name: users
- #
- # id :integer not null, primary key
- # active :boolean default(FALSE), not null
- # geometry :geometry not null, geometry, 4326
- # location :geography not null, point, 0
- #
- EOS
- end
-
- it 'works with option "with_comment"' do
- is_expected.to eq expected_result
- end
- end
- end
- end
- end
- end
- end
-
- context 'when header is "== Schema Information"' do
- let :header do
- AnnotateModels::PREFIX
- end
-
- context 'when the primary key is specified' do
- context 'when the primary_key is :id' do
- let :primary_key do
- :id
- end
-
- let :columns do
- [
- mock_column(:id, :integer),
- mock_column(:name, :string, limit: 50)
- ]
- end
-
- context 'when option "format_rdoc" is true' do
- let :options do
- { format_rdoc: true }
- end
-
- let :expected_result do
- <<~EOS
- # == Schema Information
- #
- # Table name: users
- #
- # *id*:: integer, not null, primary key
- # *name*:: string(50), not null
- #--
- # == Schema Information End
- #++
- EOS
- end
-
- it 'returns schema info in RDoc format' do
- is_expected.to eq(expected_result)
- end
- end
-
- context 'when option "format_yard" is true' do
- let :options do
- { format_yard: true }
- end
-
- let :expected_result do
- <<~EOS
- # == Schema Information
- #
- # Table name: users
- #
- # @!attribute id
- # @return [Integer]
- # @!attribute name
- # @return [String]
- #
- EOS
- end
-
- it 'returns schema info in YARD format' do
- is_expected.to eq(expected_result)
- end
- end
-
- context 'when option "format_markdown" is true' do
- context 'when other option is not specified' do
- let :options do
- { format_markdown: true }
- end
-
- let :expected_result do
- <<~EOS
- # == Schema Information
- #
- # Table name: `users`
- #
- # ### Columns
- #
- # Name | Type | Attributes
- # ----------- | ------------------ | ---------------------------
- # **`id`** | `integer` | `not null, primary key`
- # **`name`** | `string(50)` | `not null`
- #
- EOS
- end
-
- it 'returns schema info in Markdown format' do
- is_expected.to eq(expected_result)
- end
- end
-
- context 'when option "show_indexes" is true' do
- let :options do
- { format_markdown: true, show_indexes: true }
- end
-
- context 'when indexes are normal' do
- let :indexes do
- [
- mock_index('index_rails_02e851e3b7', columns: ['id']),
- mock_index('index_rails_02e851e3b8', columns: ['foreign_thing_id'])
- ]
- end
-
- let :expected_result do
- <<~EOS
- # == Schema Information
- #
- # Table name: `users`
- #
- # ### Columns
- #
- # Name | Type | Attributes
- # ----------- | ------------------ | ---------------------------
- # **`id`** | `integer` | `not null, primary key`
- # **`name`** | `string(50)` | `not null`
- #
- # ### Indexes
- #
- # * `index_rails_02e851e3b7`:
- # * **`id`**
- # * `index_rails_02e851e3b8`:
- # * **`foreign_thing_id`**
- #
- EOS
- end
-
- it 'returns schema info with index information in Markdown format' do
- is_expected.to eq expected_result
- end
- end
-
- context 'when one of indexes includes "unique" clause' do
- let :indexes do
- [
- mock_index('index_rails_02e851e3b7', columns: ['id']),
- mock_index('index_rails_02e851e3b8',
- columns: ['foreign_thing_id'],
- unique: true)
- ]
- end
-
- let :expected_result do
- <<~EOS
- # == Schema Information
- #
- # Table name: `users`
- #
- # ### Columns
- #
- # Name | Type | Attributes
- # ----------- | ------------------ | ---------------------------
- # **`id`** | `integer` | `not null, primary key`
- # **`name`** | `string(50)` | `not null`
- #
- # ### Indexes
- #
- # * `index_rails_02e851e3b7`:
- # * **`id`**
- # * `index_rails_02e851e3b8` (_unique_):
- # * **`foreign_thing_id`**
- #
- EOS
- end
-
- it 'returns schema info with index information in Markdown format' do
- is_expected.to eq expected_result
- end
- end
-
- context 'when one of indexes includes orderd index key' do
- let :indexes do
- [
- mock_index('index_rails_02e851e3b7', columns: ['id']),
- mock_index('index_rails_02e851e3b8',
- columns: ['foreign_thing_id'],
- orders: { 'foreign_thing_id' => :desc })
- ]
- end
-
- let :expected_result do
- <<~EOS
- # == Schema Information
- #
- # Table name: `users`
- #
- # ### Columns
- #
- # Name | Type | Attributes
- # ----------- | ------------------ | ---------------------------
- # **`id`** | `integer` | `not null, primary key`
- # **`name`** | `string(50)` | `not null`
- #
- # ### Indexes
- #
- # * `index_rails_02e851e3b7`:
- # * **`id`**
- # * `index_rails_02e851e3b8`:
- # * **`foreign_thing_id DESC`**
- #
- EOS
- end
-
- it 'returns schema info with index information in Markdown format' do
- is_expected.to eq expected_result
- end
- end
-
- context 'when one of indexes includes "where" clause and "unique" clause' do
- let :indexes do
- [
- mock_index('index_rails_02e851e3b7', columns: ['id']),
- mock_index('index_rails_02e851e3b8',
- columns: ['foreign_thing_id'],
- unique: true,
- where: 'name IS NOT NULL')
- ]
- end
-
- let :expected_result do
- <<~EOS
- # == Schema Information
- #
- # Table name: `users`
- #
- # ### Columns
- #
- # Name | Type | Attributes
- # ----------- | ------------------ | ---------------------------
- # **`id`** | `integer` | `not null, primary key`
- # **`name`** | `string(50)` | `not null`
- #
- # ### Indexes
- #
- # * `index_rails_02e851e3b7`:
- # * **`id`**
- # * `index_rails_02e851e3b8` (_unique_ _where_ name IS NOT NULL):
- # * **`foreign_thing_id`**
- #
- EOS
- end
-
- it 'returns schema info with index information in Markdown format' do
- is_expected.to eq expected_result
- end
- end
-
- context 'when one of indexes includes "using" clause other than "btree"' do
- let :indexes do
- [
- mock_index('index_rails_02e851e3b7', columns: ['id']),
- mock_index('index_rails_02e851e3b8',
- columns: ['foreign_thing_id'],
- using: 'hash')
- ]
- end
-
- let :expected_result do
- <<~EOS
- # == Schema Information
- #
- # Table name: `users`
- #
- # ### Columns
- #
- # Name | Type | Attributes
- # ----------- | ------------------ | ---------------------------
- # **`id`** | `integer` | `not null, primary key`
- # **`name`** | `string(50)` | `not null`
- #
- # ### Indexes
- #
- # * `index_rails_02e851e3b7`:
- # * **`id`**
- # * `index_rails_02e851e3b8` (_using_ hash):
- # * **`foreign_thing_id`**
- #
- EOS
- end
-
- it 'returns schema info with index information in Markdown format' do
- is_expected.to eq expected_result
- end
- end
- end
-
- context 'when option "show_foreign_keys" is true' do
- let :options do
- { format_markdown: true, show_foreign_keys: true }
- end
-
- let :columns do
- [
- mock_column(:id, :integer),
- mock_column(:foreign_thing_id, :integer)
- ]
- end
-
- context 'when foreign_keys have option "on_delete" and "on_update"' do
- let :foreign_keys do
- [
- mock_foreign_key('fk_rails_02e851e3b7',
- 'foreign_thing_id',
- 'foreign_things',
- 'id',
- on_delete: 'on_delete_value',
- on_update: 'on_update_value')
- ]
- end
-
- let :expected_result do
- <<~EOS
- # == Schema Information
- #
- # Table name: `users`
- #
- # ### Columns
- #
- # Name | Type | Attributes
- # ----------------------- | ------------------ | ---------------------------
- # **`id`** | `integer` | `not null, primary key`
- # **`foreign_thing_id`** | `integer` | `not null`
- #
- # ### Foreign Keys
- #
- # * `fk_rails_...` (_ON DELETE => on_delete_value ON UPDATE => on_update_value_):
- # * **`foreign_thing_id => foreign_things.id`**
- #
- EOS
- end
-
- it 'returns schema info with foreign_keys in Markdown format' do
- is_expected.to eq(expected_result)
- end
- end
- end
- end
-
- context 'when "format_doc" and "with_comment" are specified in options' do
- let :options do
- { format_rdoc: true, with_comment: true }
- end
-
- context 'when columns are normal' do
- let :columns do
- [
- mock_column(:id, :integer, comment: 'ID'),
- mock_column(:name, :string, limit: 50, comment: 'Name')
- ]
- end
-
- let :expected_result do
- <<~EOS
- # == Schema Information
- #
- # Table name: users
- #
- # *id(ID)*:: integer, not null, primary key
- # *name(Name)*:: string(50), not null
- #--
- # == Schema Information End
- #++
- EOS
- end
-
- it 'returns schema info in RDoc format' do
- is_expected.to eq expected_result
- end
- end
- end
-
- context 'when "format_markdown" and "with_comment" are specified in options' do
- let :options do
- { format_markdown: true, with_comment: true }
- end
-
- context 'when columns have comments' do
- let :columns do
- [
- mock_column(:id, :integer, comment: 'ID'),
- mock_column(:name, :string, limit: 50, comment: 'Name')
- ]
- end
-
- let :expected_result do
- <<~EOS
- # == Schema Information
- #
- # Table name: `users`
- #
- # ### Columns
- #
- # Name | Type | Attributes
- # ----------------- | ------------------ | ---------------------------
- # **`id(ID)`** | `integer` | `not null, primary key`
- # **`name(Name)`** | `string(50)` | `not null`
- #
- EOS
- end
-
- it 'returns schema info in Markdown format' do
- is_expected.to eq expected_result
- end
- end
-
- context 'when columns have multibyte comments' do
- let :columns do
- [
- mock_column(:id, :integer, comment: 'ID'),
- mock_column(:name, :string, limit: 50, comment: 'NAME')
- ]
- end
-
- let :expected_result do
- <<~EOS
- # == Schema Information
- #
- # Table name: `users`
- #
- # ### Columns
- #
- # Name | Type | Attributes
- # --------------------- | ------------------ | ---------------------------
- # **`id(ID)`** | `integer` | `not null, primary key`
- # **`name(NAME)`** | `string(50)` | `not null`
- #
- EOS
- end
-
- it 'returns schema info in Markdown format' do
- is_expected.to eq expected_result
- end
- end
- end
- end
- end
- end
- end
- end
-
describe '.set_defaults' do
subject do
Annotate::Helpers.true?(ENV['show_complete_foreign_keys'])
@@ -2428,7 +879,7 @@ class User < ActiveRecord::Base
mock_column(:id, :integer),
mock_column(:name, :string, limit: 50)
])
- @schema_info = AnnotateModels.get_schema_info(@klass, '== Schema Info')
+ @schema_info = AnnotateModels::SchemaInfo.generate(@klass, '== Schema Info')
Annotate::Helpers.reset_options(Annotate::Constants::ALL_ANNOTATE_OPTIONS)
end
@@ -2491,7 +942,7 @@ def annotate_one_file(options = {})
'id',
on_delete: :cascade)
])
- @schema_info = AnnotateModels.get_schema_info(klass, '== Schema Info', show_foreign_keys: true)
+ @schema_info = AnnotateModels::SchemaInfo.generate(klass, '== Schema Info', show_foreign_keys: true)
annotate_one_file
end
@@ -2510,7 +961,7 @@ def annotate_one_file(options = {})
'id',
on_delete: :restrict)
])
- @schema_info = AnnotateModels.get_schema_info(klass, '== Schema Info', show_foreign_keys: true)
+ @schema_info = AnnotateModels::SchemaInfo.generate(klass, '== Schema Info', show_foreign_keys: true)
annotate_one_file
expect(File.read(@model_file_name)).to eq("#{@schema_info}#{@file_content}")
end
@@ -2520,7 +971,7 @@ def annotate_one_file(options = {})
describe 'with existing annotation => :before' do
before do
annotate_one_file position: :before
- another_schema_info = AnnotateModels.get_schema_info(mock_class(:users, :id, [mock_column(:id, :integer)]), '== Schema Info')
+ another_schema_info = AnnotateModels::SchemaInfo.generate(mock_class(:users, :id, [mock_column(:id, :integer)]), '== Schema Info')
@schema_info = another_schema_info
end
@@ -2543,7 +994,7 @@ def annotate_one_file(options = {})
describe 'with existing annotation => :after' do
before do
annotate_one_file position: :after
- another_schema_info = AnnotateModels.get_schema_info(mock_class(:users, :id, [mock_column(:id, :integer)]), '== Schema Info')
+ another_schema_info = AnnotateModels::SchemaInfo.generate(mock_class(:users, :id, [mock_column(:id, :integer)]), '== Schema Info')
@schema_info = another_schema_info
end
@@ -2564,8 +1015,8 @@ def annotate_one_file(options = {})
end
it 'should skip columns with option[:ignore_columns] set' do
- output = AnnotateModels.get_schema_info(@klass, '== Schema Info',
- :ignore_columns => '(id|updated_at|created_at)')
+ output = AnnotateModels::SchemaInfo.generate(@klass, '== Schema Info',
+ :ignore_columns => '(id|updated_at|created_at)')
expect(output.match(/id/)).to be_nil
end
@@ -2581,7 +1032,7 @@ class Foo::User < ActiveRecord::Base
mock_column(:id, :integer),
mock_column(:name, :string, limit: 50)
])
- schema_info = AnnotateModels.get_schema_info(klass, '== Schema Info')
+ schema_info = AnnotateModels::SchemaInfo.generate(klass, '== Schema Info')
AnnotateModels.annotate_one_file(model_file_name, schema_info, position: :before)
expect(File.read(model_file_name)).to eq("#{schema_info}#{file_content}")
end
@@ -2611,7 +1062,7 @@ class User < ActiveRecord::Base
model_file_name, = write_model 'user.rb', "#{magic_comment}\n#{content}"
annotate_one_file position: :before
- schema_info = AnnotateModels.get_schema_info(@klass, '== Schema Info')
+ schema_info = AnnotateModels::SchemaInfo.generate(@klass, '== Schema Info')
expect(File.read(model_file_name)).to eq("#{magic_comment}\n\n#{schema_info}#{content}")
end
@@ -2620,7 +1071,7 @@ class User < ActiveRecord::Base
it 'only keeps a single empty line around the annotation (position :before)' do
content = "class User < ActiveRecord::Base\nend\n"
MAGIC_COMMENTS.each do |magic_comment|
- schema_info = AnnotateModels.get_schema_info(@klass, '== Schema Info')
+ schema_info = AnnotateModels::SchemaInfo.generate(@klass, '== Schema Info')
model_file_name, = write_model 'user.rb', "#{magic_comment}\n\n\n\n#{content}"
annotate_one_file position: :before
@@ -2635,7 +1086,7 @@ class User < ActiveRecord::Base
model_file_name, = write_model 'user.rb', "#{magic_comment}\n#{content}"
annotate_one_file position: :after
- schema_info = AnnotateModels.get_schema_info(@klass, '== Schema Info')
+ schema_info = AnnotateModels::SchemaInfo.generate(@klass, '== Schema Info')
expect(File.read(model_file_name)).to eq("#{magic_comment}\n#{content}\n#{schema_info}")
end
@@ -2692,7 +1143,7 @@ class User < ActiveRecord::Base
it "should abort with different annotation when frozen: true " do
annotate_one_file
- another_schema_info = AnnotateModels.get_schema_info(mock_class(:users, :id, [mock_column(:id, :integer)]), '== Schema Info')
+ another_schema_info = AnnotateModels::SchemaInfo.generate(mock_class(:users, :id, [mock_column(:id, :integer)]), '== Schema Info')
@schema_info = another_schema_info
expect { annotate_one_file frozen: true }.to raise_error SystemExit, /user.rb needs to be updated, but annotate was run with `--frozen`./
diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb
index 5b68b3636..376047676 100644
--- a/spec/spec_helper.rb
+++ b/spec/spec_helper.rb
@@ -33,3 +33,61 @@
require 'annotate/helpers'
require 'annotate/constants'
require 'byebug'
+
+def mock_index(name, params = {})
+ double('IndexKeyDefinition',
+ name: name,
+ columns: params[:columns] || [],
+ unique: params[:unique] || false,
+ orders: params[:orders] || {},
+ where: params[:where],
+ using: params[:using])
+end
+
+def mock_foreign_key(name, from_column, to_table, to_column = 'id', constraints = {})
+ double('ForeignKeyDefinition',
+ name: name,
+ column: from_column,
+ to_table: to_table,
+ primary_key: to_column,
+ on_delete: constraints[:on_delete],
+ on_update: constraints[:on_update])
+end
+
+def mock_connection(indexes = [], foreign_keys = [])
+ double('Conn',
+ indexes: indexes,
+ foreign_keys: foreign_keys,
+ supports_foreign_keys?: true)
+end
+
+def mock_class(table_name, primary_key, columns, indexes = [], foreign_keys = [])
+ options = {
+ connection: mock_connection(indexes, foreign_keys),
+ table_exists?: true,
+ table_name: table_name,
+ primary_key: primary_key,
+ column_names: columns.map { |col| col.name.to_s },
+ columns: columns,
+ column_defaults: Hash[columns.map { |col| [col.name, col.default] }],
+ table_name_prefix: ''
+ }
+
+ double('An ActiveRecord class', options)
+end
+
+def mock_column(name, type, options = {})
+ default_options = {
+ limit: nil,
+ null: false,
+ default: nil,
+ sql_type: type
+ }
+
+ stubs = default_options.dup
+ stubs.merge!(options)
+ stubs[:name] = name
+ stubs[:type] = type
+
+ double('Column', stubs)
+end