From 011d01cabd28c655f40dc7ee4f8cbe209fd1b658 Mon Sep 17 00:00:00 2001 From: Shu Fujita Date: Thu, 16 Jan 2020 06:19:17 +0900 Subject: [PATCH 01/33] Move helpers in annotate_models_spec.rb --- spec/lib/annotate/annotate_models_spec.rb | 58 ----------------------- spec/spec_helper.rb | 58 +++++++++++++++++++++++ 2 files changed, 58 insertions(+), 58 deletions(-) diff --git a/spec/lib/annotate/annotate_models_spec.rb b/spec/lib/annotate/annotate_models_spec.rb index d9f74cf9d..cf7ec403b 100644 --- a/spec/lib/annotate/annotate_models_spec.rb +++ b/spec/lib/annotate/annotate_models_spec.rb @@ -21,64 +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) diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index 5b68b3636..1b5fcf52c 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 From f4da457e4cec782625c8099a740324cb271f605e Mon Sep 17 00:00:00 2001 From: Shu Fujita Date: Sun, 9 Feb 2020 00:49:59 +0900 Subject: [PATCH 02/33] Clean spec_helper.rb --- spec/spec_helper.rb | 38 +++++++++++++++++++------------------- 1 file changed, 19 insertions(+), 19 deletions(-) diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index 1b5fcf52c..376047676 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -36,22 +36,22 @@ 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]) + 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]) + 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 = []) @@ -63,13 +63,13 @@ def mock_connection(indexes = [], foreign_keys = []) 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] }], + 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: '' } From daf4b0d58b8adc8b786098315dfbeada0c5c08da Mon Sep 17 00:00:00 2001 From: Shu Fujita Date: Sun, 9 Feb 2020 01:17:43 +0900 Subject: [PATCH 03/33] Execute `rubocop --auto-gen-config` --- .rubocop_todo.yml | 6 +- lib/annotate/annotate_models.rb | 461 +---- lib/annotate/annotate_models/schema_info.rb | 461 +++++ .../templates/auto_annotate_models.rake | 4 +- .../annotate_models/schema_info_spec.rb | 1497 ++++++++++++++++ spec/lib/annotate/annotate_models_spec.rb | 1513 +---------------- 6 files changed, 1976 insertions(+), 1966 deletions(-) create mode 100644 lib/annotate/annotate_models/schema_info.rb create mode 100644 spec/lib/annotate/annotate_models/schema_info_spec.rb diff --git a/.rubocop_todo.yml b/.rubocop_todo.yml index 246a23ea6..bd0bc6a30 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 15:49:55 +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. diff --git a/lib/annotate/annotate_models.rb b/lib/annotate/annotate_models.rb index 469283f89..98d8fb864 100644 --- a/lib/annotate/annotate_models.rb +++ b/lib/annotate/annotate_models.rb @@ -4,6 +4,7 @@ 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 +12,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 +61,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 +187,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 +435,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..7ce91aef4 --- /dev/null +++ b/lib/annotate/annotate_models/schema_info.rb @@ -0,0 +1,461 @@ +module AnnotateModels + module SchemaInfo + # 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 = {}) + 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 + + 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.any? { |col| !col.comment.nil? } + 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) + 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 + + 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 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 = 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 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 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 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 + + 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 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 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 width(string) + string.chars.inject(0) { |acc, elem| acc + (elem.bytesize == 3 ? 2 : 1) } + 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 + 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..82814a78b --- /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 cf7ec403b..113d11790 100644 --- a/spec/lib/annotate/annotate_models_spec.rb +++ b/spec/lib/annotate/annotate_models_spec.rb @@ -21,70 +21,6 @@ '# -*- frozen_string_literal : true -*-' ].freeze - 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 { @@ -118,1433 +54,6 @@ 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']) @@ -2370,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 @@ -2433,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 @@ -2452,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 @@ -2462,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 @@ -2485,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 @@ -2506,7 +1015,7 @@ def annotate_one_file(options = {}) end it 'should skip columns with option[:ignore_columns] set' do - output = AnnotateModels.get_schema_info(@klass, '== Schema Info', + output = AnnotateModels::SchemaInfo.generate(@klass, '== Schema Info', :ignore_columns => '(id|updated_at|created_at)') expect(output.match(/id/)).to be_nil end @@ -2523,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 @@ -2553,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 @@ -2562,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 @@ -2577,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 @@ -2634,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`./ From 1722e61ace8c704585d829cca9389f20a35c9246 Mon Sep 17 00:00:00 2001 From: Shu Fujita Date: Sun, 9 Feb 2020 16:04:01 +0900 Subject: [PATCH 04/33] Execute `rubocop -a --only Layout/SpaceInsideBlockBraces` --- lib/annotate/annotate_models/schema_info.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/annotate/annotate_models/schema_info.rb b/lib/annotate/annotate_models/schema_info.rb index 7ce91aef4..b583e5be9 100644 --- a/lib/annotate/annotate_models/schema_info.rb +++ b/lib/annotate/annotate_models/schema_info.rb @@ -256,7 +256,7 @@ def get_index_info(klass, options = {}) indexes = retrieve_indexes_from_table(klass) return '' if indexes.empty? - max_size = indexes.collect{|index| index.name.size}.max + 1 + 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) @@ -347,7 +347,7 @@ def get_foreign_key_info(klass, options = {}) 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| + 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 From a358af845a5a736ca29f789e34866ca1805cfde9 Mon Sep 17 00:00:00 2001 From: Shu Fujita Date: Sun, 9 Feb 2020 16:04:44 +0900 Subject: [PATCH 05/33] Execute `rubocop -a --only Style/TrailingCommaInArrayLiteral` --- spec/lib/annotate/annotate_models/schema_info_spec.rb | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/spec/lib/annotate/annotate_models/schema_info_spec.rb b/spec/lib/annotate/annotate_models/schema_info_spec.rb index 82814a78b..99061ee73 100644 --- a/spec/lib/annotate/annotate_models/schema_info_spec.rb +++ b/spec/lib/annotate/annotate_models/schema_info_spec.rb @@ -92,7 +92,7 @@ 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), + mock_column(:decimal, :decimal, unsigned?: true, precision: 10, scale: 2) ] end @@ -186,7 +186,7 @@ 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), + mock_column(:title, :string, limit: 50) ]) end @@ -199,7 +199,7 @@ let :columns do [ mock_column(:id, :integer, limit: 8), - mock_column(:author_name, :string, limit: 50), + mock_column(:author_name, :string, limit: 50) ] end From 700a9f9d5c06e331bed5311ed07f13969574a4f6 Mon Sep 17 00:00:00 2001 From: Shu Fujita Date: Sun, 9 Feb 2020 16:09:18 +0900 Subject: [PATCH 06/33] Execute `rubocop -a --only Style/FormatString` --- lib/annotate/annotate_models/schema_info.rb | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/lib/annotate/annotate_models/schema_info.rb b/lib/annotate/annotate_models/schema_info.rb index b583e5be9..9e973ddaa 100644 --- a/lib/annotate/annotate_models/schema_info.rb +++ b/lib/annotate/annotate_models/schema_info.rb @@ -39,7 +39,7 @@ def generate(klass, header, options = {}) 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 << 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 @@ -54,7 +54,7 @@ def generate(klass, header, options = {}) end if options[:format_rdoc] - info << sprintf("# %-#{max_size}.#{max_size}s%s", "*#{col_name}*::", attrs.unshift(col_type).join(", ")).rstrip + "\n" + 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) @@ -62,7 +62,7 @@ def generate(klass, header, options = {}) 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" + 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 @@ -269,7 +269,7 @@ def get_index_info(klass, options = {}) end def final_index_string_in_markdown(index) - details = sprintf( + details = format( "%s%s%s", index_unique_info(index, :markdown), index_where_info(index, :markdown), @@ -277,7 +277,7 @@ def final_index_string_in_markdown(index) ).strip details = " (#{details})" unless details.blank? - sprintf( + format( "# * `%s`%s:\n# * **`%s`**\n", index.name, details, @@ -286,7 +286,7 @@ def final_index_string_in_markdown(index) end def final_index_string(index, max_size) - sprintf( + format( "# %-#{max_size}.#{max_size}s %s%s%s%s", index.name, "(#{index_columns_info(index).join(',')})", @@ -355,9 +355,9 @@ def get_foreign_key_info(klass, options = {}) 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) + format("# * `%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" + format("# %-#{max_size}.#{max_size}s %s %s", format_name.call(fk), "(#{ref_info})", constraints_info).rstrip + "\n" end end @@ -376,7 +376,7 @@ def get_schema_footer_text(_klass, options = {}) 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" + 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) From 6c35ddf15d2b7305faddcfc839f48ee142e2ca86 Mon Sep 17 00:00:00 2001 From: Shu Fujita Date: Sun, 9 Feb 2020 16:10:26 +0900 Subject: [PATCH 07/33] Execute `rubocop -a --only Layout/SpaceBeforeBlockBraces` --- lib/annotate/annotate_models/schema_info.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/annotate/annotate_models/schema_info.rb b/lib/annotate/annotate_models/schema_info.rb index 9e973ddaa..cdfe72c2a 100644 --- a/lib/annotate/annotate_models/schema_info.rb +++ b/lib/annotate/annotate_models/schema_info.rb @@ -256,7 +256,7 @@ def get_index_info(klass, options = {}) indexes = retrieve_indexes_from_table(klass) return '' if indexes.empty? - max_size = indexes.collect{ |index| index.name.size }.max + 1 + 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) From e37a85787dc6bfb8b598dacdb7ed0957d08583d3 Mon Sep 17 00:00:00 2001 From: Shu Fujita Date: Sun, 9 Feb 2020 16:11:16 +0900 Subject: [PATCH 08/33] Execute `rubocop -a --only Layout/SpaceAroundOperators` --- lib/annotate/annotate_models/schema_info.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/annotate/annotate_models/schema_info.rb b/lib/annotate/annotate_models/schema_info.rb index cdfe72c2a..017afa4ab 100644 --- a/lib/annotate/annotate_models/schema_info.rb +++ b/lib/annotate/annotate_models/schema_info.rb @@ -57,7 +57,7 @@ def generate(klass, header, options = {}) 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) + 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) @@ -385,7 +385,7 @@ def mb_chars_ljust(string, length) if padding > 0 string + (' ' * padding) else - string[0..length-1] + string[0..length - 1] end end From 8ad5f822900374ef753d8dab7c3dea234096d08e Mon Sep 17 00:00:00 2001 From: Shu Fujita Date: Sun, 9 Feb 2020 16:12:13 +0900 Subject: [PATCH 09/33] Execute `rubocop -a --only Layout/SpaceBeforeComment` --- lib/annotate/annotate_models/schema_info.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/annotate/annotate_models/schema_info.rb b/lib/annotate/annotate_models/schema_info.rb index 017afa4ab..bb82e30ae 100644 --- a/lib/annotate/annotate_models/schema_info.rb +++ b/lib/annotate/annotate_models/schema_info.rb @@ -179,7 +179,7 @@ def get_attributes(column, column_type, klass, options) # 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 + 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| From f629eb9e0a98429d567fa1bf46bb213e2b788385 Mon Sep 17 00:00:00 2001 From: Shu Fujita Date: Sun, 9 Feb 2020 16:14:47 +0900 Subject: [PATCH 10/33] Execute `rubocop -a --only Style/StringLiterals` --- lib/annotate/annotate_models/schema_info.rb | 10 +++---- .../annotate_models/schema_info_spec.rb | 28 +++++++++---------- 2 files changed, 19 insertions(+), 19 deletions(-) diff --git a/lib/annotate/annotate_models/schema_info.rb b/lib/annotate/annotate_models/schema_info.rb index bb82e30ae..af068a9d0 100644 --- a/lib/annotate/annotate_models/schema_info.rb +++ b/lib/annotate/annotate_models/schema_info.rb @@ -54,7 +54,7 @@ def generate(klass, header, options = {}) end if options[:format_rdoc] - info << format("# %-#{max_size}.#{max_size}s%s", "*#{col_name}*::", attrs.unshift(col_type).join(", ")).rstrip + "\n" + 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) @@ -62,7 +62,7 @@ def generate(klass, header, options = {}) 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" + 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 @@ -185,7 +185,7 @@ def get_attributes(column, column_type, klass, options) 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(", ")}]") + attrs << (ind.empty? ? 'indexed' : "indexed => [#{ind.join(", ")}]") end end end @@ -270,7 +270,7 @@ def get_index_info(klass, options = {}) def final_index_string_in_markdown(index) details = format( - "%s%s%s", + '%s%s%s', index_unique_info(index, :markdown), index_where_info(index, :markdown), index_using_info(index, :markdown) @@ -376,7 +376,7 @@ def get_schema_footer_text(_klass, options = {}) 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" + 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) diff --git a/spec/lib/annotate/annotate_models/schema_info_spec.rb b/spec/lib/annotate/annotate_models/schema_info_spec.rb index 99061ee73..a06872626 100644 --- a/spec/lib/annotate/annotate_models/schema_info_spec.rb +++ b/spec/lib/annotate/annotate_models/schema_info_spec.rb @@ -314,10 +314,10 @@ 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) + mock_column('id', :integer), + mock_column('firstname', :string), + mock_column('surname', :string), + mock_column('value', :string) ] end @@ -357,10 +357,10 @@ 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) + mock_column('id', :integer), + mock_column('firstname', :string), + mock_column('surname', :string), + mock_column('value', :string) ] end @@ -400,10 +400,10 @@ 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) + mock_column('id', :integer), + mock_column('firstname', :string), + mock_column('surname', :string), + mock_column('value', :string) ] end @@ -512,8 +512,8 @@ context 'when one of indexes is in string form' do let :columns do [ - mock_column("id", :integer), - mock_column("name", :string) + mock_column('id', :integer), + mock_column('name', :string) ] end From 091550200ab752a7ba8879faab0207c27c7f2386 Mon Sep 17 00:00:00 2001 From: Shu Fujita Date: Sun, 9 Feb 2020 15:59:46 +0900 Subject: [PATCH 11/33] Execute `rubocop -a --only Style/PercentLiteralDelimiters` --- lib/annotate/annotate_models/schema_info.rb | 4 ++-- spec/lib/annotate/annotate_models/schema_info_spec.rb | 6 +++--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/lib/annotate/annotate_models/schema_info.rb b/lib/annotate/annotate_models/schema_info.rb index af068a9d0..8e0c842b6 100644 --- a/lib/annotate/annotate_models/schema_info.rb +++ b/lib/annotate/annotate_models/schema_info.rb @@ -1,11 +1,11 @@ module AnnotateModels module SchemaInfo # Don't show default value for these column types - NO_DEFAULT_COL_TYPES = %w(json jsonb hstore).freeze + 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 + NO_LIMIT_COL_TYPES = %w[integer bigint boolean].freeze INDEX_CLAUSES = { unique: { diff --git a/spec/lib/annotate/annotate_models/schema_info_spec.rb b/spec/lib/annotate/annotate_models/schema_info_spec.rb index a06872626..3a844792d 100644 --- a/spec/lib/annotate/annotate_models/schema_info_spec.rb +++ b/spec/lib/annotate/annotate_models/schema_info_spec.rb @@ -325,7 +325,7 @@ [ mock_index('index_rails_02e851e3b7', columns: ['id']), mock_index('index_rails_02e851e3b8', - columns: %w(firstname surname value), + columns: %w[firstname surname value], orders: { 'surname' => :asc, 'value' => :desc }) ] end @@ -368,7 +368,7 @@ [ mock_index('index_rails_02e851e3b7', columns: ['id']), mock_index('index_rails_02e851e3b8', - columns: %w(firstname surname), + columns: %w[firstname surname], where: 'value IS NOT NULL') ] end @@ -411,7 +411,7 @@ [ mock_index('index_rails_02e851e3b7', columns: ['id']), mock_index('index_rails_02e851e3b8', - columns: %w(firstname surname), + columns: %w[firstname surname], using: 'hash') ] end From 8a6b9ae7a67ef457a317b2723bd89da101c3f09b Mon Sep 17 00:00:00 2001 From: Shu Fujita Date: Sun, 9 Feb 2020 16:17:01 +0900 Subject: [PATCH 12/33] Execute `rubocop -a --only Layout/AlignArguments` --- spec/lib/annotate/annotate_models_spec.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spec/lib/annotate/annotate_models_spec.rb b/spec/lib/annotate/annotate_models_spec.rb index 113d11790..3c24d16bf 100644 --- a/spec/lib/annotate/annotate_models_spec.rb +++ b/spec/lib/annotate/annotate_models_spec.rb @@ -1016,7 +1016,7 @@ def annotate_one_file(options = {}) it 'should skip columns with option[:ignore_columns] set' do output = AnnotateModels::SchemaInfo.generate(@klass, '== Schema Info', - :ignore_columns => '(id|updated_at|created_at)') + :ignore_columns => '(id|updated_at|created_at)') expect(output.match(/id/)).to be_nil end From 1a97f55b8fc6e6db33c475f0c52e9822be40a555 Mon Sep 17 00:00:00 2001 From: Shu Fujita Date: Sun, 9 Feb 2020 16:17:51 +0900 Subject: [PATCH 13/33] Execute `rubocop -a --only Layout/SpaceInsideParens` --- lib/annotate/annotate_models/schema_info.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/annotate/annotate_models/schema_info.rb b/lib/annotate/annotate_models/schema_info.rb index 8e0c842b6..2a329430b 100644 --- a/lib/annotate/annotate_models/schema_info.rb +++ b/lib/annotate/annotate_models/schema_info.rb @@ -39,8 +39,8 @@ def generate(klass, header, options = {}) 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" + 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) From dafa682b54a4b8578e742b6be8e0a92b7ada7945 Mon Sep 17 00:00:00 2001 From: Shu Fujita Date: Sun, 9 Feb 2020 16:19:00 +0900 Subject: [PATCH 14/33] Execute `rubocop -a --only Layout/SpaceInsideStringInterpolation` --- lib/annotate/annotate_models/schema_info.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/annotate/annotate_models/schema_info.rb b/lib/annotate/annotate_models/schema_info.rb index 2a329430b..25c33bb36 100644 --- a/lib/annotate/annotate_models/schema_info.rb +++ b/lib/annotate/annotate_models/schema_info.rb @@ -40,7 +40,7 @@ def generate(klass, header, options = {}) 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" + info << "# #{'-' * (max_size + md_names_overhead)} | #{'-' * md_type_allowance} | #{'-' * 27}\n" end cols = columns(klass, options) From 9345d24734f0094b17175697fa775ec0ecd81891 Mon Sep 17 00:00:00 2001 From: Shu Fujita Date: Sun, 9 Feb 2020 16:28:08 +0900 Subject: [PATCH 15/33] Execute `rubocop -a --only Style/StringLiteralsInInterpolation` --- lib/annotate/annotate_models/schema_info.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/annotate/annotate_models/schema_info.rb b/lib/annotate/annotate_models/schema_info.rb index 25c33bb36..bda16aa37 100644 --- a/lib/annotate/annotate_models/schema_info.rb +++ b/lib/annotate/annotate_models/schema_info.rb @@ -48,7 +48,7 @@ def generate(klass, header, options = {}) 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")})" + "#{col.name}(#{col.comment.gsub(/\n/, '\\n')})" else col.name end @@ -185,7 +185,7 @@ def get_attributes(column, column_type, klass, options) 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(", ")}]") + attrs << (ind.empty? ? 'indexed' : "indexed => [#{ind.join(', ')}]") end end end From 6ed7231c85d4d8d1eaef7c494778373461381235 Mon Sep 17 00:00:00 2001 From: Shu Fujita Date: Sun, 9 Feb 2020 16:21:24 +0900 Subject: [PATCH 16/33] Execute `rubocop -a --only Style/RedundantParentheses` --- lib/annotate/annotate_models/schema_info.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/annotate/annotate_models/schema_info.rb b/lib/annotate/annotate_models/schema_info.rb index bda16aa37..d1c1f07da 100644 --- a/lib/annotate/annotate_models/schema_info.rb +++ b/lib/annotate/annotate_models/schema_info.rb @@ -62,7 +62,7 @@ def generate(klass, header, options = {}) 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" + 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 From f0b8bd91a456e2c5e125913fd4030bbcda578001 Mon Sep 17 00:00:00 2001 From: Shu Fujita Date: Sun, 9 Feb 2020 16:24:54 +0900 Subject: [PATCH 17/33] Execute `rubocop -a --only Style/IfUnlessModifier` --- lib/annotate/annotate_models/schema_info.rb | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/lib/annotate/annotate_models/schema_info.rb b/lib/annotate/annotate_models/schema_info.rb index d1c1f07da..2fcaf107f 100644 --- a/lib/annotate/annotate_models/schema_info.rb +++ b/lib/annotate/annotate_models/schema_info.rb @@ -68,13 +68,9 @@ def generate(klass, header, options = {}) end end - if options[:show_indexes] && klass.table_exists? - info << get_index_info(klass, options) - end + info << get_index_info(klass, options) if options[:show_indexes] && klass.table_exists? - if options[:show_foreign_keys] && klass.table_exists? - info << get_foreign_key_info(klass, options) - end + info << get_foreign_key_info(klass, options) if options[:show_foreign_keys] && klass.table_exists? info << get_schema_footer_text(klass, options) end From 145597196dadfee08630fc1c1df5309759071541 Mon Sep 17 00:00:00 2001 From: Shu Fujita Date: Sun, 9 Feb 2020 16:26:27 +0900 Subject: [PATCH 18/33] Execute `rubocop -a --only Layout/MultilineOperationIndentation` --- lib/annotate/annotate_models/schema_info.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/annotate/annotate_models/schema_info.rb b/lib/annotate/annotate_models/schema_info.rb index 2fcaf107f..3129ecb1d 100644 --- a/lib/annotate/annotate_models/schema_info.rb +++ b/lib/annotate/annotate_models/schema_info.rb @@ -332,7 +332,7 @@ def get_foreign_key_info(klass, options = {}) end return '' unless klass.connection.respond_to?(:supports_foreign_keys?) && - klass.connection.supports_foreign_keys? && klass.connection.respond_to?(: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? From a96a3b07790f0cd293deb60e52f108fe6ee3d3a1 Mon Sep 17 00:00:00 2001 From: Shu Fujita Date: Sun, 9 Feb 2020 16:27:09 +0900 Subject: [PATCH 19/33] Execute `rubocop -a --only Layout/EmptyLineAfterGuardClause` --- lib/annotate/annotate_models/schema_info.rb | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/annotate/annotate_models/schema_info.rb b/lib/annotate/annotate_models/schema_info.rb index 3129ecb1d..85f0c1554 100644 --- a/lib/annotate/annotate_models/schema_info.rb +++ b/lib/annotate/annotate_models/schema_info.rb @@ -339,6 +339,7 @@ def get_foreign_key_info(klass, options = {}) 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 From 705c8368801013e559d29ab13d5e5f5f8a4d149f Mon Sep 17 00:00:00 2001 From: Shu Fujita Date: Mon, 10 Feb 2020 16:57:56 +0900 Subject: [PATCH 20/33] Execute `rubocop -a --only Layout/ExtraSpacing` --- lib/annotate/annotate_models/schema_info.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/annotate/annotate_models/schema_info.rb b/lib/annotate/annotate_models/schema_info.rb index 85f0c1554..c789b6a4d 100644 --- a/lib/annotate/annotate_models/schema_info.rb +++ b/lib/annotate/annotate_models/schema_info.rb @@ -373,7 +373,7 @@ def get_schema_footer_text(_klass, options = {}) 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" + 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) From 28c29fd774629a67c2eabdb098b36e0b103e8b3f Mon Sep 17 00:00:00 2001 From: Shu Fujita Date: Mon, 10 Feb 2020 16:59:45 +0900 Subject: [PATCH 21/33] Fix Rubocop Style/NumericPredicate violation --- lib/annotate/annotate_models/schema_info.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/annotate/annotate_models/schema_info.rb b/lib/annotate/annotate_models/schema_info.rb index c789b6a4d..0ce3701f8 100644 --- a/lib/annotate/annotate_models/schema_info.rb +++ b/lib/annotate/annotate_models/schema_info.rb @@ -379,7 +379,7 @@ def format_default(col_name, max_size, col_type, bare_type_allowance, attrs) def mb_chars_ljust(string, length) string = string.to_s padding = length - width(string) - if padding > 0 + if padding.positive? string + (' ' * padding) else string[0..length - 1] From 42dd1a50cdcea4c5a36bb5b6b08faf89ebc04c8c Mon Sep 17 00:00:00 2001 From: Shu Fujita Date: Mon, 10 Feb 2020 17:00:53 +0900 Subject: [PATCH 22/33] Fix Rubocop Lint/AssignmentInCondition violation --- lib/annotate/annotate_models/schema_info.rb | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/lib/annotate/annotate_models/schema_info.rb b/lib/annotate/annotate_models/schema_info.rb index 0ce3701f8..569978b23 100644 --- a/lib/annotate/annotate_models/schema_info.rb +++ b/lib/annotate/annotate_models/schema_info.rb @@ -411,7 +411,8 @@ def columns(klass, options) cols = klass.columns cols += translated_columns(klass) - if ignore_columns = options[:ignore_columns] + ignore_columns = options[:ignore_columns] + if ignore_columns cols = cols.reject do |col| col.name.match(/#{ignore_columns}/) end From be8c3d92f4c7051e475d166337cc61c66ff82247 Mon Sep 17 00:00:00 2001 From: Shu Fujita Date: Sun, 9 Feb 2020 15:52:36 +0900 Subject: [PATCH 23/33] Refactor AnnotateModels::SchemaInfo.generate --- lib/annotate/annotate_models/schema_info.rb | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/lib/annotate/annotate_models/schema_info.rb b/lib/annotate/annotate_models/schema_info.rb index 569978b23..1aac1d807 100644 --- a/lib/annotate/annotate_models/schema_info.rb +++ b/lib/annotate/annotate_models/schema_info.rb @@ -177,12 +177,11 @@ def get_attributes(column, column_type, klass, options) # 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 + indices.select { |ind| ind.columns.include? column.name }&.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 From c3dfca50f50cf09056fe84d1075d085711f1321a Mon Sep 17 00:00:00 2001 From: Shu Fujita Date: Mon, 10 Feb 2020 17:02:36 +0900 Subject: [PATCH 24/33] Execute `rubocop --auto-gen-config` --- .rubocop_todo.yml | 120 ++++++++++------------------------------------ 1 file changed, 24 insertions(+), 96 deletions(-) diff --git a/.rubocop_todo.yml b/.rubocop_todo.yml index bd0bc6a30..59ede06ee 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-07 15:49:55 +0900 using RuboCop version 0.68.1. +# on 2020-04-07 16:19:23 +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 @@ -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: @@ -203,16 +155,21 @@ Metrics/BlockNesting: # Offense count: 12 Metrics/CyclomaticComplexity: - Max: 25 + Max: 24 -# Offense count: 30 +# Offense count: 31 # Configuration parameters: CountComments, ExcludedMethods. Metrics/MethodLength: - Max: 40 + Max: 36 + +# Offense count: 1 +# Configuration parameters: CountComments. +Metrics/ModuleLength: + Max: 363 # Offense count: 9 Metrics/PerceivedComplexity: - Max: 28 + Max: 27 # Offense count: 1 Naming/AccessorMethodName: @@ -224,6 +181,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 +231,7 @@ Style/Dir: Exclude: - 'bin/annotate' -# Offense count: 10 +# Offense count: 11 Style/Documentation: Exclude: - 'spec/**/*' @@ -281,6 +239,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 +260,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 +289,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 +333,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 +343,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 +352,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 +369,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. @@ -477,7 +421,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 +435,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 +448,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 From 083bb0db00ccf151bf55ae241a52770b0790f635 Mon Sep 17 00:00:00 2001 From: Shu Fujita Date: Thu, 16 Jan 2020 06:30:16 +0900 Subject: [PATCH 25/33] Clean style of AnnotateModels::SchemaInfo --- lib/annotate/annotate_models/schema_info.rb | 33 ++++++++++++++++----- 1 file changed, 26 insertions(+), 7 deletions(-) diff --git a/lib/annotate/annotate_models/schema_info.rb b/lib/annotate/annotate_models/schema_info.rb index 1aac1d807..ef9aee372 100644 --- a/lib/annotate/annotate_models/schema_info.rb +++ b/lib/annotate/annotate_models/schema_info.rb @@ -39,7 +39,10 @@ def generate(klass, header, options = {}) 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 << 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 @@ -54,7 +57,9 @@ def generate(klass, header, options = {}) end if options[:format_rdoc] - info << format("# %-#{max_size}.#{max_size}s%s", "*#{col_name}*::", attrs.unshift(col_type).join(', ')).rstrip + "\n" + 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) @@ -62,7 +67,12 @@ def generate(klass, header, options = {}) 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" + 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 @@ -351,9 +361,15 @@ def get_foreign_key_info(klass, options = {}) 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) + 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" + format("# %-#{max_size}.#{max_size}s %s %s", + format_name.call(fk), + "(#{ref_info})", + constraints_info).rstrip + "\n" end end @@ -372,7 +388,10 @@ def get_schema_footer_text(_klass, options = {}) 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" + 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) @@ -381,7 +400,7 @@ def mb_chars_ljust(string, length) if padding.positive? string + (' ' * padding) else - string[0..length - 1] + string[0..(length - 1)] end end From e26bcfafe92a93e16c0837e2755f58838c752621 Mon Sep 17 00:00:00 2001 From: Shu Fujita Date: Mon, 10 Feb 2020 19:46:02 +0900 Subject: [PATCH 26/33] Remove comment for rubocop:disable in AnnotateModels --- lib/annotate/annotate_models.rb | 2 -- 1 file changed, 2 deletions(-) diff --git a/lib/annotate/annotate_models.rb b/lib/annotate/annotate_models.rb index 98d8fb864..ef26087c3 100644 --- a/lib/annotate/annotate_models.rb +++ b/lib/annotate/annotate_models.rb @@ -1,5 +1,3 @@ -# rubocop:disable Metrics/ModuleLength - require 'bigdecimal' require 'annotate/constants' From 9def568d7b60ecf46e9d54718593d4c6e0ffbb5b Mon Sep 17 00:00:00 2001 From: Shu Fujita Date: Mon, 10 Feb 2020 19:52:39 +0900 Subject: [PATCH 27/33] Add comments for rubocop:disable --- lib/annotate/annotate_models/schema_info.rb | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/annotate/annotate_models/schema_info.rb b/lib/annotate/annotate_models/schema_info.rb index ef9aee372..7fe7cbe98 100644 --- a/lib/annotate/annotate_models/schema_info.rb +++ b/lib/annotate/annotate_models/schema_info.rb @@ -1,5 +1,5 @@ module AnnotateModels - module SchemaInfo + module SchemaInfo # rubocop:disable Metrics/ModuleLength # Don't show default value for these column types NO_DEFAULT_COL_TYPES = %w[json jsonb hstore].freeze @@ -29,7 +29,7 @@ class << self # 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 = {}) + def generate(klass, header, options = {}) # rubocop:disable Metrics/MethodLength info = "# #{header}\n" info << get_schema_header_text(klass, options) @@ -47,7 +47,7 @@ def generate(klass, header, options = {}) end cols = columns(klass, options) - cols.each do |col| + cols.each do |col| # rubocop:disable Metrics/BlockLength col_type = get_col_type(col) attrs = get_attributes(col, col_type, klass, options) col_name = if with_comments?(klass, options) && col.comment From eab6b0a30943735d7724d495e81f0134950dbc34 Mon Sep 17 00:00:00 2001 From: Shu Fujita Date: Mon, 10 Feb 2020 22:15:04 +0900 Subject: [PATCH 28/33] Execute `rubocop --auto-gen-config` --- .rubocop_todo.yml | 16 +++++----------- 1 file changed, 5 insertions(+), 11 deletions(-) diff --git a/.rubocop_todo.yml b/.rubocop_todo.yml index 59ede06ee..1c0b1de22 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-07 16:19:23 +0900 using RuboCop version 0.68.1. +# on 2020-04-07 16:20:29 +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 @@ -122,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: @@ -157,15 +151,15 @@ Metrics/BlockNesting: Metrics/CyclomaticComplexity: Max: 24 -# Offense count: 31 +# Offense count: 30 # Configuration parameters: CountComments, ExcludedMethods. Metrics/MethodLength: - Max: 36 + Max: 33 # Offense count: 1 # Configuration parameters: CountComments. Metrics/ModuleLength: - Max: 363 + Max: 303 # Offense count: 9 Metrics/PerceivedComplexity: @@ -454,7 +448,7 @@ Style/UnneededPercentQ: Exclude: - 'annotate.gemspec' -# Offense count: 394 +# Offense count: 393 # Cop supports --auto-correct. # Configuration parameters: AutoCorrect, AllowHeredoc, AllowURI, URISchemes, IgnoreCopDirectives, IgnoredPatterns. # URISchemes: http, https From cf77430a00ed9e5c647c4ce931b8e453d2a6e7d2 Mon Sep 17 00:00:00 2001 From: Shu Fujita Date: Thu, 13 Feb 2020 19:55:19 +0900 Subject: [PATCH 29/33] Refactor AnnotateModels::SchemaInfo.with_comments? --- lib/annotate/annotate_models/schema_info.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/annotate/annotate_models/schema_info.rb b/lib/annotate/annotate_models/schema_info.rb index 7fe7cbe98..3f8d93654 100644 --- a/lib/annotate/annotate_models/schema_info.rb +++ b/lib/annotate/annotate_models/schema_info.rb @@ -118,7 +118,7 @@ def max_schema_info_width(klass, options) def with_comments?(klass, options) options[:with_comment] && klass.columns.first.respond_to?(:comment) && - klass.columns.any? { |col| !col.comment.nil? } + klass.columns.map(&:comment).any? { |comment| !comment.nil? } end def classified_sort(cols) From aea68f2c1956a46d8f50a6da99e534aec90a974f Mon Sep 17 00:00:00 2001 From: Shu Fujita Date: Wed, 26 Feb 2020 20:24:48 +0900 Subject: [PATCH 30/33] Remove safe navigation operator (&) for Ruby 2.2 --- lib/annotate/annotate_models/schema_info.rb | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/lib/annotate/annotate_models/schema_info.rb b/lib/annotate/annotate_models/schema_info.rb index 3f8d93654..6b7785057 100644 --- a/lib/annotate/annotate_models/schema_info.rb +++ b/lib/annotate/annotate_models/schema_info.rb @@ -186,12 +186,14 @@ def get_attributes(column, column_type, klass, options) # 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) - indices.select { |ind| ind.columns.include? column.name }&.sort_by(&:name)&.each do |ind| - next if ind.columns.is_a?(String) + indices = retrieve_indexes_from_table(klass).select { |ind| ind.columns.include? column.name } + if indices + 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(', ')}]") + ind = ind.columns.reject! { |i| i == column.name } + attrs << (ind.empty? ? 'indexed' : "indexed => [#{ind.join(', ')}]") + end end end From 6e5af1a9c2e16b7e6dd8e8596de14fd5ae23a9c3 Mon Sep 17 00:00:00 2001 From: Shu Fujita Date: Wed, 26 Feb 2020 20:27:19 +0900 Subject: [PATCH 31/33] Execute `rubocop --auto-gen-config` --- .rubocop_todo.yml | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/.rubocop_todo.yml b/.rubocop_todo.yml index 1c0b1de22..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-07 16:20:29 +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 @@ -149,7 +149,7 @@ Metrics/BlockNesting: # Offense count: 12 Metrics/CyclomaticComplexity: - Max: 24 + Max: 25 # Offense count: 30 # Configuration parameters: CountComments, ExcludedMethods. @@ -163,7 +163,7 @@ Metrics/ModuleLength: # Offense count: 9 Metrics/PerceivedComplexity: - Max: 27 + Max: 28 # Offense count: 1 Naming/AccessorMethodName: @@ -399,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. @@ -448,7 +449,7 @@ Style/UnneededPercentQ: Exclude: - 'annotate.gemspec' -# Offense count: 393 +# Offense count: 394 # Cop supports --auto-correct. # Configuration parameters: AutoCorrect, AllowHeredoc, AllowURI, URISchemes, IgnoreCopDirectives, IgnoredPatterns. # URISchemes: http, https From 4edcb79686b66a5db286c7267a6112c36a714067 Mon Sep 17 00:00:00 2001 From: Shu Fujita Date: Tue, 7 Apr 2020 16:30:24 +0900 Subject: [PATCH 32/33] Remove unnecessary comment for disabling Rubocop --- lib/annotate/annotate_models/schema_info.rb | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/lib/annotate/annotate_models/schema_info.rb b/lib/annotate/annotate_models/schema_info.rb index 6b7785057..3a64ce7de 100644 --- a/lib/annotate/annotate_models/schema_info.rb +++ b/lib/annotate/annotate_models/schema_info.rb @@ -47,7 +47,7 @@ def generate(klass, header, options = {}) # rubocop:disable Metrics/MethodLength end cols = columns(klass, options) - cols.each do |col| # rubocop:disable Metrics/BlockLength + 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 @@ -187,13 +187,11 @@ def get_attributes(column, column_type, klass, options) # 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 } - if indices - indices.sort_by(&:name).each do |ind| - next if ind.columns.is_a?(String) + 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 + ind = ind.columns.reject! { |i| i == column.name } + attrs << (ind.empty? ? 'indexed' : "indexed => [#{ind.join(', ')}]") end end From 5e1fe825ebae62ab8642537253ee07b33b609c1f Mon Sep 17 00:00:00 2001 From: Shu Fujita Date: Tue, 7 Apr 2020 16:32:51 +0900 Subject: [PATCH 33/33] Sort methods in order of appearance --- lib/annotate/annotate_models/schema_info.rb | 196 ++++++++++---------- 1 file changed, 98 insertions(+), 98 deletions(-) diff --git a/lib/annotate/annotate_models/schema_info.rb b/lib/annotate/annotate_models/schema_info.rb index 3a64ce7de..caf434338 100644 --- a/lib/annotate/annotate_models/schema_info.rb +++ b/lib/annotate/annotate_models/schema_info.rb @@ -121,6 +121,58 @@ def with_comments?(klass, options) 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 = [] @@ -251,6 +303,40 @@ def retrieve_indexes_from_table(klass) 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" @@ -301,8 +387,14 @@ def final_index_string(index, max_size) ).rstrip + "\n" end - def index_unique_info(index, format = :default) - index.unique ? " #{INDEX_CLAUSES[:unique][format]}" : '' + 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) @@ -314,6 +406,10 @@ def index_where_info(index, format = :default) 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 @@ -323,16 +419,6 @@ def index_using_info(index, format = :default) 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 get_foreign_key_info(klass, options = {}) fk_info = if options[:format_markdown] "#\n# ### Foreign Keys\n#\n" @@ -386,92 +472,6 @@ def get_schema_footer_text(_klass, options = {}) info << "#\n" end 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 width(string) - string.chars.inject(0) { |acc, elem| acc + (elem.bytesize == 3 ? 2 : 1) } - 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) - - 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 end end end