diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 953cc7013..efeab9dec 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -24,14 +24,17 @@ jobs:
uses: ruby/setup-ruby@v1
with:
ruby-version: ${{ matrix.ruby }}
- bundler-cache: true
+ bundler-cache: false
- name: Install sqlite
run: |
sudo apt-get install libsqlite3-dev
+ - name: Install dependencies
+ run: bundle install
+
- name: Run Tests
run: INTEGRATION_TESTS=1 bundle exec rspec
- - name: Rubocop
- run: bundle exec rubocop
+ # - name: Rubocop
+ # run: bundle exec rubocop
diff --git a/.rubocop_todo.yml b/.rubocop_todo.yml
index 246a23ea6..2df92fdbe 100644
--- a/.rubocop_todo.yml
+++ b/.rubocop_todo.yml
@@ -27,7 +27,7 @@ Gemspec/RequiredRubyVersion:
# SupportedHashRocketStyles: key, separator, table
# SupportedColonStyles: key, separator, table
# SupportedLastArgumentHashStyles: always_inspect, always_ignore, ignore_implicit, ignore_explicit
-Layout/AlignHash:
+Layout/HashAlignment:
Exclude:
- 'lib/generators/annotate/templates/auto_annotate_models.rake'
- 'spec/lib/annotate/annotate_models_spec.rb'
@@ -68,7 +68,7 @@ Layout/ExtraSpacing:
# Cop supports --auto-correct.
# Configuration parameters: IndentationWidth.
# SupportedStyles: special_inside_parentheses, consistent, align_brackets
-Layout/IndentFirstArrayElement:
+Layout/FirstArrayElementIndentation:
EnforcedStyle: consistent
# Offense count: 5
@@ -158,7 +158,7 @@ Lint/AssignmentInCondition:
- 'lib/annotate/annotate_models.rb'
# Offense count: 1
-Lint/HandleExceptions:
+Lint/SuppressedException:
Exclude:
- 'bin/annotate'
@@ -237,7 +237,7 @@ Naming/MemoizedInstanceVariableName:
# Offense count: 1
# Configuration parameters: MinNameLength, AllowNamesEndingInNumbers, AllowedNames, ForbiddenNames.
# AllowedNames: io, id, to, by, on, in, at, ip, db
-Naming/UncommunicativeMethodParamName:
+Naming/MethodParameterName:
Exclude:
- 'Rakefile'
@@ -354,7 +354,7 @@ Style/InverseMethods:
- 'Rakefile'
# Offense count: 1
-Style/MethodMissingSuper:
+Lint/MissingSuper:
Exclude:
- 'lib/annotate/active_record_patch.rb'
@@ -522,7 +522,7 @@ Style/TrailingCommaInArrayLiteral:
# Offense count: 2
# Cop supports --auto-correct.
-Style/UnneededPercentQ:
+Style/RedundantPercentQ:
Exclude:
- 'annotate.gemspec'
diff --git a/.tool-versions b/.tool-versions
new file mode 100644
index 000000000..9b6768d19
--- /dev/null
+++ b/.tool-versions
@@ -0,0 +1 @@
+ruby 2.6.7
diff --git a/Gemfile b/Gemfile
index 184fe0ba4..4acd9cb84 100644
--- a/Gemfile
+++ b/Gemfile
@@ -1,6 +1,6 @@
source 'https://rubygems.org'
-ruby '>= 2.2.0'
+ruby '>= 2.4.0'
gem 'activerecord', '>= 4.2.5', '< 6', require: false
gem 'rake', require: false
@@ -20,7 +20,10 @@ group :development, :test do
gem 'rspec', require: false
gem 'rubocop', '~> 0.68.1', require: false unless RUBY_VERSION =~ /^1.8/
- gem 'simplecov', require: false
+ # gem 'rubocop', '~> 1.12', require: false
+ # gem 'rubocop-rake', require: false
+ # gem 'rubocop-rspec', require: false
+ # gem 'simplecov', require: false
gem 'terminal-notifier-guard', require: false
gem 'codeclimate-test-reporter'
diff --git a/Guardfile b/Guardfile
index 602c7b57c..f93bc135e 100644
--- a/Guardfile
+++ b/Guardfile
@@ -1,4 +1,4 @@
-# Note: The cmd option is now required due to the increasing number of ways
+# NOTE: The cmd option is now required due to the increasing number of ways
# rspec may be run, below are examples of the most common uses.
# * bundler: 'bundle exec rspec'
# * bundler binstubs: 'bin/rspec'
diff --git a/Rakefile b/Rakefile
index bfbd51b7d..43944f2dc 100644
--- a/Rakefile
+++ b/Rakefile
@@ -3,7 +3,7 @@ def exit_exception(e)
exit e.status_code
end
-# Note : this causes annoying psych warnings under Ruby 1.9.2-p180; to fix, upgrade to 1.9.3
+# NOTE: this causes annoying psych warnings under Ruby 1.9.2-p180; to fix, upgrade to 1.9.3
begin
require 'bundler'
Bundler.setup(:default, :development)
@@ -162,7 +162,7 @@ namespace :integration do
fixtures[Digest::MD5.hexdigest(File.read(fname))] = File.expand_path(fname)
end
- candidates.keys.each do |digest|
+ candidates.each_key do |digest|
next unless fixtures.key?(digest)
candidates[digest].each do |fname|
# Double-check contents in case of hash collision...
diff --git a/lib/annotate/annotate_models.rb b/lib/annotate/annotate_models.rb
index 1d84b7945..abcc41a47 100644
--- a/lib/annotate/annotate_models.rb
+++ b/lib/annotate/annotate_models.rb
@@ -53,7 +53,7 @@ def model_dir
@model_dir.is_a?(Array) ? @model_dir : [@model_dir || 'app/models']
end
- attr_writer :model_dir
+ attr_writer :model_dir, :root_dir, :skip_subdirectory_model_load
def root_dir
if @root_dir.blank?
@@ -65,8 +65,6 @@ def root_dir
end
end
- attr_writer :root_dir
-
def skip_subdirectory_model_load
# This option is set in options[:skip_subdirectory_model_load]
# and stops the get_loaded_model method from loading a model from a subdir
@@ -78,8 +76,6 @@ def skip_subdirectory_model_load
end
end
- attr_writer :skip_subdirectory_model_load
-
def get_patterns(options, pattern_types = [])
current_patterns = []
root_dir.each do |root_directory|
@@ -89,7 +85,7 @@ def get_patterns(options, pattern_types = [])
current_patterns += if pattern_type.to_sym == :additional_file_patterns
patterns
else
- patterns.map { |p| p.sub(/^[\/]*/, '') }
+ patterns.map { |p| p.sub(/^\/*/, '') }
end
end
end
@@ -156,15 +152,15 @@ def get_schema_info(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 << "#{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"
+ 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"
+ 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"
+ 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
@@ -345,7 +341,7 @@ def get_foreign_key_info(klass, options = {})
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"
+ "#{sprintf("# %-#{max_size}.#{max_size}s %s %s", format_name.call(fk), "(#{ref_info})", constraints_info).rstrip}\n"
end
end
@@ -371,11 +367,11 @@ def annotate_one_file(file_name, info_block, position, options = {})
return false if old_content =~ /#{SKIP_ANNOTATION_PREFIX}.*\n/
# Ignore the Schema version line because it changes with each migration
- header_pattern = /(^# Table name:.*?\n(#.*[\r]?\n)*[\r]?)/
+ header_pattern = /(^# Table name:.*?\n(#.*\r?\n)*\r?)/
old_header = old_content.match(header_pattern).to_s
new_header = info_block.match(header_pattern).to_s
- column_pattern = /^#[\t ]+[\w\*\.`]+[\t ]+.+$/
+ column_pattern = /^#[\t ]+[\w*.`]+[\t ]+.+$/
old_columns = old_header && old_header.scan(column_pattern).sort
new_columns = new_header && new_header.scan(column_pattern).sort
@@ -398,11 +394,11 @@ def annotate_one_file(file_name, info_block, position, options = {})
old_content.sub!(annotate_pattern(options), '')
new_content = if %w(after bottom).include?(options[position].to_s)
- magic_comments_block + (old_content.rstrip + "\n\n" + wrapped_info_block)
+ magic_comments_block + ("#{old_content.rstrip}\n\n#{wrapped_info_block}")
elsif magic_comments_block.empty?
magic_comments_block + wrapped_info_block + old_content.lstrip
else
- magic_comments_block + "\n" + wrapped_info_block + old_content.lstrip
+ "#{magic_comments_block}\n#{wrapped_info_block}#{old_content.lstrip}"
end
else
# replace the old annotation with the new one
@@ -512,7 +508,7 @@ def annotate(klass, file, header, options = {})
end
rescue StandardError => e
$stderr.puts "Unable to annotate #{file}: #{e.message}"
- $stderr.puts "\t" + e.backtrace.join("\n\t") if options[:trace]
+ $stderr.puts "\t#{e.backtrace.join("\n\t")}" if options[:trace]
end
annotated
@@ -589,7 +585,7 @@ def get_model_class(file)
if File.file?(file_path) && Kernel.require(file_path)
retry
elsif model_path =~ /\//
- model_path = model_path.split('/')[1..-1].join('/').to_s
+ model_path = model_path.split('/')[1..].join('/').to_s
retry
else
raise
@@ -625,7 +621,7 @@ def get_loaded_model_by_path(model_path)
# Revert to the old way but it is not really robust
ObjectSpace.each_object(::Class)
.select do |c|
- Class === c && # note: we use === to avoid a bug in activesupport 2.3.14 OptionMerger vs. is_a?
+ Class === c && # NOTE: we use === to avoid a bug in activesupport 2.3.14 OptionMerger vs. is_a?
c.ancestors.respond_to?(:include?) && # to fix FactoryGirl bug, see https://github.com/ctran/annotate_models/pull/82
c.ancestors.include?(ActiveRecord::Base)
end.detect { |c| ActiveSupport::Inflector.underscore(c.to_s) == model_path }
@@ -685,11 +681,11 @@ def annotate_model_file(annotated, file, header, options)
rescue BadModelFileError => e
unless options[:ignore_unknown_models]
$stderr.puts "Unable to annotate #{file}: #{e.message}"
- $stderr.puts "\t" + e.backtrace.join("\n\t") if options[:trace]
+ $stderr.puts "\t#{e.backtrace.join("\n\t")}" if options[:trace]
end
rescue StandardError => e
$stderr.puts "Unable to annotate #{file}: #{e.message}"
- $stderr.puts "\t" + e.backtrace.join("\n\t") if options[:trace]
+ $stderr.puts "\t#{e.backtrace.join("\n\t")}" if options[:trace]
end
end
@@ -720,7 +716,7 @@ def remove_annotations(options = {})
deannotated << klass if deannotated_klass
rescue StandardError => e
$stderr.puts "Unable to deannotate #{File.join(file)}: #{e.message}"
- $stderr.puts "\t" + e.backtrace.join("\n\t") if options[:trace]
+ $stderr.puts "\t#{e.backtrace.join("\n\t")}" if options[:trace]
end
end
puts "Removed annotations from: #{deannotated.join(', ')}"
@@ -780,7 +776,7 @@ def max_schema_info_width(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"
+ "#{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)
diff --git a/lib/annotate/annotate_routes.rb b/lib/annotate/annotate_routes.rb
index 75cc421ed..38499d5e9 100644
--- a/lib/annotate/annotate_routes.rb
+++ b/lib/annotate/annotate_routes.rb
@@ -67,9 +67,10 @@ def routes_file
end
def strip_on_removal(content, header_position)
- if header_position == :before
+ case header_position
+ when :before
content.shift while content.first == ''
- elsif header_position == :after
+ when :after
content.pop while content.last == ''
end
diff --git a/lib/annotate/annotate_routes/header_generator.rb b/lib/annotate/annotate_routes/header_generator.rb
index b1c93acf7..4cd7c60b1 100644
--- a/lib/annotate/annotate_routes/header_generator.rb
+++ b/lib/annotate/annotate_routes/header_generator.rb
@@ -21,7 +21,7 @@ def routes_map(options)
# In old versions of Rake, the first line of output was the cwd. Not so
# much in newer ones. We ditch that line if it exists, and if not, we
# keep the line around.
- result.shift if result.first =~ %r{^\(in \/}
+ result.shift if result.first =~ %r{^\(in /}
ignore_routes = options[:ignore_routes]
regexp_for_ignoring_routes = ignore_routes ? /#{ignore_routes}/ : nil
@@ -57,7 +57,7 @@ def generate
out << comment
return out if contents_without_magic_comments.size.zero?
- maxs = [HEADER_ROW.map(&:size)] + contents_without_magic_comments[1..-1].map { |line| line.split.map(&:size) }
+ maxs = [HEADER_ROW.map(&:size)] + contents_without_magic_comments[1..].map { |line| line.split.map(&:size) }
if markdown?
max = maxs.map(&:max).compact.max
@@ -68,7 +68,7 @@ def generate
out << comment(content(contents_without_magic_comments[0], maxs))
end
- out += contents_without_magic_comments[1..-1].map { |line| comment(content(markdown? ? line.split(' ') : line, maxs)) }
+ out += contents_without_magic_comments[1..].map { |line| comment(content(markdown? ? line.split(' ') : line, maxs)) }
out << comment(options[:wrapper_close]) if options[:wrapper_close]
out
diff --git a/spec/lib/annotate/annotate_models_spec.rb b/spec/lib/annotate/annotate_models_spec.rb
index 370298f3c..d286cbb72 100644
--- a/spec/lib/annotate/annotate_models_spec.rb
+++ b/spec/lib/annotate/annotate_models_spec.rb
@@ -1,2503 +1,28 @@
# encoding: utf-8
require_relative '../../spec_helper'
+require_relative 'models/model_spec_helper'
require 'annotate/annotate_models'
require 'annotate/active_record_patch'
require 'active_support/core_ext/string'
require 'files'
require 'tmpdir'
-describe AnnotateModels do
- MAGIC_COMMENTS = [
- '# encoding: UTF-8',
- '# coding: UTF-8',
- '# -*- coding: UTF-8 -*-',
- '#encoding: utf-8',
- '# encoding: utf-8',
- '# -*- encoding : utf-8 -*-',
- "# encoding: utf-8\n# frozen_string_literal: true",
- "# frozen_string_literal: true\n# encoding: utf-8",
- '# frozen_string_literal: true',
- '#frozen_string_literal: false',
- '# -*- frozen_string_literal : true -*-'
- ].freeze
-
- def mock_index(name, params = {})
- double('IndexKeyDefinition',
- name: name,
- columns: params[:columns] || [],
- unique: params[:unique] || false,
- orders: params[:orders] || {},
- where: params[:where],
- using: params[:using])
- end
-
- def mock_foreign_key(name, from_column, to_table, to_column = 'id', constraints = {})
- double('ForeignKeyDefinition',
- name: name,
- column: from_column,
- to_table: to_table,
- primary_key: to_column,
- on_delete: constraints[:on_delete],
- on_update: constraints[:on_update])
- end
-
- def mock_connection(indexes = [], foreign_keys = [])
- double('Conn',
- indexes: indexes,
- foreign_keys: foreign_keys,
- supports_foreign_keys?: true)
- end
-
- def mock_class(table_name, primary_key, columns, indexes = [], foreign_keys = [])
- options = {
- connection: mock_connection(indexes, foreign_keys),
- table_exists?: true,
- table_name: table_name,
- primary_key: primary_key,
- column_names: columns.map { |col| col.name.to_s },
- columns: columns,
- column_defaults: Hash[columns.map { |col| [col.name, col.default] }],
- table_name_prefix: ''
- }
-
- double('An ActiveRecord class', options)
- end
-
- def mock_column(name, type, options = {})
- default_options = {
- limit: nil,
- null: false,
- default: nil,
- sql_type: type
- }
-
- stubs = default_options.dup
- stubs.merge!(options)
- stubs[:name] = name
- stubs[:type] = type
-
- double('Column', stubs)
- end
-
- describe '.quote' do
- subject do
- AnnotateModels.quote(value)
- end
-
- context 'when the argument is nil' do
- let(:value) { nil }
- it 'returns string "NULL"' do
- is_expected.to eq('NULL')
- end
- end
-
- context 'when the argument is true' do
- let(:value) { true }
- it 'returns string "TRUE"' do
- is_expected.to eq('TRUE')
- end
- end
-
- context 'when the argument is false' do
- let(:value) { false }
- it 'returns string "FALSE"' do
- is_expected.to eq('FALSE')
- end
- end
-
- context 'when the argument is an integer' do
- let(:value) { 25 }
- it 'returns the integer as a string' do
- is_expected.to eq('25')
- end
- end
-
- context 'when the argument is a float number' do
- context 'when the argument is like 25.6' do
- let(:value) { 25.6 }
- it 'returns the float number as a string' do
- is_expected.to eq('25.6')
- end
- end
-
- context 'when the argument is like 1e-20' do
- let(:value) { 1e-20 }
- it 'returns the float number as a string' do
- is_expected.to eq('1.0e-20')
- end
- end
- end
-
- context 'when the argument is a BigDecimal number' do
- let(:value) { BigDecimal('1.2') }
- it 'returns the float number as a string' do
- is_expected.to eq('1.2')
- end
- end
-
- context 'when the argument is an array' do
- let(:value) { [BigDecimal('1.2')] }
- it 'returns an array of which elements are converted to string' do
- is_expected.to eq(['1.2'])
- end
- end
- end
-
- describe '.parse_options' do
- let(:options) do
- {
- root_dir: '/root',
- model_dir: 'app/models,app/one, app/two ,,app/three',
- skip_subdirectory_model_load: false
- }
- end
-
- before :each do
- AnnotateModels.send(:parse_options, options)
- end
-
- describe '@root_dir' do
- subject do
- AnnotateModels.instance_variable_get(:@root_dir)
- end
-
- it 'sets @root_dir' do
- is_expected.to eq('/root')
- end
- end
-
- describe '@model_dir' do
- subject do
- AnnotateModels.instance_variable_get(:@model_dir)
- end
-
- it 'separates option "model_dir" with commas and sets @model_dir as an array of string' do
- is_expected.to eq(['app/models', 'app/one', 'app/two', 'app/three'])
- end
- end
-
- describe '@skip_subdirectory_model_load' do
- subject do
- AnnotateModels.instance_variable_get(:@skip_subdirectory_model_load)
- end
-
- context 'option is set to true' do
- let(:options) do
- {
- root_dir: '/root',
- model_dir: 'app/models,app/one, app/two ,,app/three',
- skip_subdirectory_model_load: true
- }
- end
-
- it 'sets skip_subdirectory_model_load to true' do
- is_expected.to eq(true)
- end
- end
-
- context 'option is set to false' do
- let(:options) do
- {
- root_dir: '/root',
- model_dir: 'app/models,app/one, app/two ,,app/three',
- skip_subdirectory_model_load: false
- }
- end
-
- it 'sets skip_subdirectory_model_load to false' do
- is_expected.to eq(false)
- end
- end
- 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'])
- end
-
- context 'when default value of "show_complete_foreign_keys" is not set' do
- it 'returns false' do
- is_expected.to be(false)
- end
- end
-
- context 'when default value of "show_complete_foreign_keys" is set' do
- before do
- Annotate.set_defaults('show_complete_foreign_keys' => 'true')
- end
-
- it 'returns true' do
- is_expected.to be(true)
- end
- end
-
- after :each do
- ENV.delete('show_complete_foreign_keys')
- end
- end
-
- describe '.get_patterns' do
- subject { AnnotateModels.get_patterns(options, pattern_type) }
-
- context 'when pattern_type is "additional_file_patterns"' do
- let(:pattern_type) { 'additional_file_patterns' }
-
- context 'when additional_file_patterns is specified in the options' do
- let(:additional_file_patterns) do
- [
- '/%PLURALIZED_MODEL_NAME%/**/*.rb',
- '/bar/%PLURALIZED_MODEL_NAME%/*_form'
- ]
- end
-
- let(:options) { { additional_file_patterns: additional_file_patterns } }
-
- it 'returns additional_file_patterns in the argument "options"' do
- is_expected.to eq(additional_file_patterns)
- end
- end
-
- context 'when additional_file_patterns is not specified in the options' do
- let(:options) { {} }
-
- it 'returns an empty array' do
- is_expected.to eq([])
- end
- end
- end
- end
-
- describe '.get_model_files' do
- subject { described_class.get_model_files(options) }
-
- before do
- ARGV.clear
-
- described_class.model_dir = [model_dir]
- end
-
- context 'when `model_dir` is valid' do
- let(:model_dir) do
- Files do
- file 'foo.rb'
- dir 'bar' do
- file 'baz.rb'
- dir 'qux' do
- file 'quux.rb'
- end
- end
- dir 'concerns' do
- file 'corge.rb'
- end
- end
- end
-
- context 'when the model files are not specified' do
- context 'when no option is specified' do
- let(:options) { {} }
-
- it 'returns all model files under `model_dir` directory' do
- is_expected.to contain_exactly(
- [model_dir, 'foo.rb'],
- [model_dir, File.join('bar', 'baz.rb')],
- [model_dir, File.join('bar', 'qux', 'quux.rb')]
- )
- end
- end
-
- context 'when `ignore_model_sub_dir` option is enabled' do
- let(:options) { { ignore_model_sub_dir: true } }
-
- it 'returns model files just below `model_dir` directory' do
- is_expected.to contain_exactly([model_dir, 'foo.rb'])
- end
- end
- end
-
- context 'when the model files are specified' do
- let(:additional_model_dir) { 'additional_model' }
- let(:model_files) do
- [
- File.join(model_dir, 'foo.rb'),
- "./#{File.join(additional_model_dir, 'corge/grault.rb')}" # Specification by relative path
- ]
- end
-
- before { ARGV.concat(model_files) }
-
- context 'when no option is specified' do
- let(:options) { {} }
-
- context 'when all the specified files are in `model_dir` directory' do
- before do
- described_class.model_dir << additional_model_dir
- end
-
- it 'returns specified files' do
- is_expected.to contain_exactly(
- [model_dir, 'foo.rb'],
- [additional_model_dir, 'corge/grault.rb']
- )
- end
- end
-
- context 'when a model file outside `model_dir` directory is specified' do
- it 'exits with the status code' do
- begin
- subject
- raise
- rescue SystemExit => e
- expect(e.status).to eq(1)
- end
- end
- end
- end
-
- context 'when `is_rake` option is enabled' do
- let(:options) { { is_rake: true } }
-
- it 'returns all model files under `model_dir` directory' do
- is_expected.to contain_exactly(
- [model_dir, 'foo.rb'],
- [model_dir, File.join('bar', 'baz.rb')],
- [model_dir, File.join('bar', 'qux', 'quux.rb')]
- )
- end
- end
- end
- end
-
- context 'when `model_dir` is invalid' do
- let(:model_dir) { '/not_exist_path' }
- let(:options) { {} }
-
- it 'exits with the status code' do
- begin
- subject
- raise
- rescue SystemExit => e
- expect(e.status).to eq(1)
- end
- end
- end
- end
-
- describe '.get_model_class' do
- before :all do
- AnnotateModels.model_dir = Dir.mktmpdir('annotate_models')
- end
-
- # TODO: use 'files' gem instead
- def create(filename, file_content)
- File.join(AnnotateModels.model_dir[0], filename).tap do |path|
- FileUtils.mkdir_p(File.dirname(path))
- File.open(path, 'wb') do |f|
- f.puts(file_content)
- end
- end
- end
-
- before :each do
- create(filename, file_content)
- end
-
- let :klass do
- AnnotateModels.get_model_class(File.join(AnnotateModels.model_dir[0], filename))
- end
-
- context 'when class Foo is defined in "foo.rb"' do
- let :filename do
- 'foo.rb'
- end
-
- let :file_content do
- <<~EOS
- class Foo < ActiveRecord::Base
- end
- EOS
- end
-
- it 'works' do
- expect(klass.name).to eq('Foo')
- end
- end
-
- context 'when class name is not capitalized normally' do
- context 'when class FooWithCAPITALS is defined in "foo_with_capitals.rb"' do
- let :filename do
- 'foo_with_capitals.rb'
- end
-
- let :file_content do
- <<~EOS
- class FooWithCAPITALS < ActiveRecord::Base
- end
- EOS
- end
-
- it 'works' do
- expect(klass.name).to eq('FooWithCAPITALS')
- end
- end
- end
-
- context 'when class is defined inside module' do
- context 'when class Bar::FooInsideBar is defined in "bar/foo_inside_bar.rb"' do
- let :filename do
- 'bar/foo_inside_bar.rb'
- end
-
- let :file_content do
- <<~EOS
- module Bar
- class FooInsideBar < ActiveRecord::Base
- end
- end
- EOS
- end
-
- it 'works' do
- expect(klass.name).to eq('Bar::FooInsideBar')
- end
- end
- end
-
- context 'when class is defined inside module and class name is not capitalized normally' do
- context 'when class Bar::FooInsideCapitalsBAR is defined in "bar/foo_inside_capitals_bar.rb"' do
- let :filename do
- 'bar/foo_inside_capitals_bar.rb'
- end
-
- let :file_content do
- <<~EOS
- module BAR
- class FooInsideCapitalsBAR < ActiveRecord::Base
- end
- end
- EOS
- end
-
- it 'works' do
- expect(klass.name).to eq('BAR::FooInsideCapitalsBAR')
- end
- end
- end
-
- context 'when unknown macros exist in class' do
- context 'when class FooWithMacro is defined in "foo_with_macro.rb"' do
- let :filename do
- 'foo_with_macro.rb'
- end
-
- let :file_content do
- <<~EOS
- class FooWithMacro < ActiveRecord::Base
- acts_as_awesome :yah
- end
- EOS
- end
-
- it 'works and does not care about known macros' do
- expect(klass.name).to eq('FooWithMacro')
- end
- end
-
- context 'when class name is with ALL CAPS segments' do
- context 'when class is "FooWithCAPITALS" is defined in "foo_with_capitals.rb"' do
- let :filename do
- 'foo_with_capitals.rb'
- end
-
- let :file_content do
- <<~EOS
- class FooWithCAPITALS < ActiveRecord::Base
- acts_as_awesome :yah
- end
- EOS
- end
-
- it 'works' do
- expect(klass.name).to eq('FooWithCAPITALS')
- end
- end
- end
- end
-
- context 'when known macros exist in class' do
- context 'when class FooWithKnownMacro is defined in "foo_with_known_macro.rb"' do
- let :filename do
- 'foo_with_known_macro.rb'
- end
-
- let :file_content do
- <<~EOS
- class FooWithKnownMacro < ActiveRecord::Base
- has_many :yah
- end
- EOS
- end
-
- it 'works and does not care about known macros' do
- expect(klass.name).to eq('FooWithKnownMacro')
- end
- end
- end
-
- context 'when the file includes invalid multibyte chars (USASCII)' do
- context 'when class FooWithUtf8 is defined in "foo_with_utf8.rb"' do
- let :filename do
- 'foo_with_utf8.rb'
- end
-
- let :file_content do
- <<~EOS
- # encoding: utf-8
- class FooWithUtf8 < ActiveRecord::Base
- UTF8STRINGS = %w[résumé façon âge]
- end
- EOS
- end
-
- it 'works without complaining of invalid multibyte chars' do
- expect(klass.name).to eq('FooWithUtf8')
- end
- end
- end
-
- context 'when non-namespaced model is inside subdirectory' do
- context 'when class NonNamespacedFooInsideBar is defined in "bar/non_namespaced_foo_inside_bar.rb"' do
- let :filename do
- 'bar/non_namespaced_foo_inside_bar.rb'
- end
-
- let :file_content do
- <<~EOS
- class NonNamespacedFooInsideBar < ActiveRecord::Base
- end
- EOS
- end
-
- it 'works' do
- expect(klass.name).to eq('NonNamespacedFooInsideBar')
- end
- end
-
- context 'when class name is not capitalized normally' do
- context 'when class NonNamespacedFooWithCapitalsInsideBar is defined in "bar/non_namespaced_foo_with_capitals_inside_bar.rb"' do
- let :filename do
- 'bar/non_namespaced_foo_with_capitals_inside_bar.rb'
- end
-
- let :file_content do
- <<~EOS
- class NonNamespacedFooWithCapitalsInsideBar < ActiveRecord::Base
- end
- EOS
- end
-
- it 'works' do
- expect(klass.name).to eq('NonNamespacedFooWithCapitalsInsideBar')
- end
- end
- end
- end
-
- context 'when class file is loaded twice' do
- context 'when class LoadedClass is defined in "loaded_class.rb"' do
- let :filename do
- 'loaded_class.rb'
- end
-
- let :file_content do
- <<~EOS
- class LoadedClass < ActiveRecord::Base
- CONSTANT = 1
- end
- EOS
- end
-
- before :each do
- path = File.expand_path(filename, AnnotateModels.model_dir[0])
- Kernel.load(path)
- expect(Kernel).not_to receive(:require)
- end
-
- it 'does not require model file twice' do
- expect(klass.name).to eq('LoadedClass')
- end
- end
-
- context 'when class is defined in a subdirectory' do
- dir = Array.new(8) { (0..9).to_a.sample(random: Random.new) }.join
-
- context "when class SubdirLoadedClass is defined in \"#{dir}/subdir_loaded_class.rb\"" do
- before :each do
- $LOAD_PATH.unshift(File.join(AnnotateModels.model_dir[0], dir))
-
- path = File.expand_path(filename, AnnotateModels.model_dir[0])
- Kernel.load(path)
- expect(Kernel).not_to receive(:require)
- end
-
- let :filename do
- "#{dir}/subdir_loaded_class.rb"
- end
-
- let :file_content do
- <<~EOS
- class SubdirLoadedClass < ActiveRecord::Base
- CONSTANT = 1
- end
- EOS
- end
-
- it 'does not require model file twice' do
- expect(klass.name).to eq('SubdirLoadedClass')
- end
- end
- end
- end
-
- context 'when two class exist' do
- before :each do
- create(filename_2, file_content_2)
- end
-
- context 'the base names are duplicated' do
- let :filename do
- 'foo.rb'
- end
-
- let :file_content do
- <<-EOS
- class Foo < ActiveRecord::Base
- end
- EOS
- end
-
- let :filename_2 do
- 'bar/foo.rb'
- end
-
- let :file_content_2 do
- <<-EOS
- class Bar::Foo < ActiveRecord::Base
- end
- EOS
- end
-
- let :klass_2 do
- AnnotateModels.get_model_class(File.join(AnnotateModels.model_dir[0], filename_2))
- end
-
- it 'finds valid model' do
- expect(klass.name).to eq('Foo')
- expect(klass_2.name).to eq('Bar::Foo')
- end
- end
-
- context 'the class name and base name clash' do
- let :filename do
- 'foo.rb'
- end
-
- let :file_content do
- <<-EOS
- class Foo < ActiveRecord::Base
- end
- EOS
- end
-
- let :filename_2 do
- 'bar/foo.rb'
- end
-
- let :file_content_2 do
- <<-EOS
- class Bar::Foo < ActiveRecord::Base
- end
- EOS
- end
-
- let :klass_2 do
- AnnotateModels.get_model_class(File.join(AnnotateModels.model_dir[0], filename_2))
- end
-
- it 'finds valid model' do
- expect(klass.name).to eq('Foo')
- expect(klass_2.name).to eq('Bar::Foo')
- end
-
- it 'attempts to load the model path without expanding if skip_subdirectory_model_load is false' do
- allow(AnnotateModels).to receive(:skip_subdirectory_model_load).and_return(false)
- full_path = File.join(AnnotateModels.model_dir[0], filename_2)
- expect(File).to_not receive(:expand_path).with(full_path)
- AnnotateModels.get_model_class(full_path)
- end
-
- it 'does not attempt to load the model path without expanding if skip_subdirectory_model_load is true' do
- $LOAD_PATH.unshift(AnnotateModels.model_dir[0])
- allow(AnnotateModels).to receive(:skip_subdirectory_model_load).and_return(true)
- full_path = File.join(AnnotateModels.model_dir[0], filename_2)
- expect(File).to receive(:expand_path).with(full_path).and_call_original
- AnnotateModels.get_model_class(full_path)
- end
- end
-
- context 'one of the classes is nested in another class' do
- let :filename do
- 'voucher.rb'
- end
-
- let :file_content do
- <<-EOS
- class Voucher < ActiveRecord::Base
- end
- EOS
- end
-
- let :filename_2 do
- 'voucher/foo.rb'
- end
-
- let :file_content_2 do
- <<~EOS
- class Voucher
- class Foo < ActiveRecord::Base
- end
- end
- EOS
- end
-
- let :klass_2 do
- AnnotateModels.get_model_class(File.join(AnnotateModels.model_dir[0], filename_2))
- end
-
- it 'finds valid model' do
- expect(klass.name).to eq('Voucher')
- expect(klass_2.name).to eq('Voucher::Foo')
- end
- end
- end
- end
-
- describe '.remove_annotation_of_file' do
- subject do
- AnnotateModels.remove_annotation_of_file(path)
- end
-
- let :tmpdir do
- Dir.mktmpdir('annotate_models')
- end
-
- let :path do
- File.join(tmpdir, filename).tap do |path|
- File.open(path, 'w') do |f|
- f.puts(file_content)
- end
- end
- end
-
- let :file_content_after_removal do
- subject
- File.read(path)
- end
-
- let :expected_result do
- <<~EOS
- class Foo < ActiveRecord::Base
- end
- EOS
- end
-
- context 'when annotation is before main content' do
- let :filename do
- 'before.rb'
- end
-
- let :file_content do
- <<~EOS
- # == Schema Information
- #
- # Table name: foo
- #
- # id :integer not null, primary key
- # created_at :datetime
- # updated_at :datetime
- #
-
- class Foo < ActiveRecord::Base
- end
- EOS
- end
-
- it 'removes annotation' do
- expect(file_content_after_removal).to eq expected_result
- end
- end
-
- context 'when annotation is before main content and CRLF is used for line breaks' do
- let :filename do
- 'before.rb'
- end
-
- let :file_content do
- <<~EOS
- # == Schema Information
- #
- # Table name: foo\r\n#
- # id :integer not null, primary key
- # created_at :datetime
- # updated_at :datetime
- #
- \r\n
- class Foo < ActiveRecord::Base
- end
- EOS
- end
-
- it 'removes annotation' do
- expect(file_content_after_removal).to eq expected_result
- end
- end
-
- context 'when annotation is before main content and with opening wrapper' do
- let :filename do
- 'opening_wrapper.rb'
- end
-
- let :file_content do
- <<~EOS
- # wrapper
- # == Schema Information
- #
- # Table name: foo
- #
- # id :integer not null, primary key
- # created_at :datetime
- # updated_at :datetime
- #
-
- class Foo < ActiveRecord::Base
- end
- EOS
- end
-
- subject do
- AnnotateModels.remove_annotation_of_file(path, wrapper_open: 'wrapper')
- end
-
- it 'removes annotation' do
- expect(file_content_after_removal).to eq expected_result
- end
- end
-
- context 'when annotation is before main content and with opening wrapper' do
- let :filename do
- 'opening_wrapper.rb'
- end
-
- let :file_content do
- <<~EOS
- # wrapper\r\n# == Schema Information
- #
- # Table name: foo
- #
- # id :integer not null, primary key
- # created_at :datetime
- # updated_at :datetime
- #
-
- class Foo < ActiveRecord::Base
- end
- EOS
- end
-
- subject do
- AnnotateModels.remove_annotation_of_file(path, wrapper_open: 'wrapper')
- end
-
- it 'removes annotation' do
- expect(file_content_after_removal).to eq expected_result
- end
- end
-
- context 'when annotation is after main content' do
- let :filename do
- 'after.rb'
- end
+MAGIC_COMMENTS = [
+ '# encoding: UTF-8',
+ '# coding: UTF-8',
+ '# -*- coding: UTF-8 -*-',
+ '#encoding: utf-8',
+ '# encoding: utf-8',
+ '# -*- encoding : utf-8 -*-',
+ "# encoding: utf-8\n# frozen_string_literal: true",
+ "# frozen_string_literal: true\n# encoding: utf-8",
+ '# frozen_string_literal: true',
+ '#frozen_string_literal: false',
+ '# -*- frozen_string_literal : true -*-'
+].freeze
- let :file_content do
- <<~EOS
- class Foo < ActiveRecord::Base
- end
-
- # == Schema Information
- #
- # Table name: foo
- #
- # id :integer not null, primary key
- # created_at :datetime
- # updated_at :datetime
- #
-
- EOS
- end
-
- it 'removes annotation' do
- expect(file_content_after_removal).to eq expected_result
- end
- end
-
- context 'when annotation is after main content and with closing wrapper' do
- let :filename do
- 'closing_wrapper.rb'
- end
-
- let :file_content do
- <<~EOS
- class Foo < ActiveRecord::Base
- end
-
- # == Schema Information
- #
- # Table name: foo
- #
- # id :integer not null, primary key
- # created_at :datetime
- # updated_at :datetime
- #
- # wrapper
-
- EOS
- end
-
- subject do
- AnnotateModels.remove_annotation_of_file(path, wrapper_close: 'wrapper')
- end
-
- it 'removes annotation' do
- expect(file_content_after_removal).to eq expected_result
- end
- end
-
- context 'when annotation is before main content and with comment "-*- SkipSchemaAnnotations"' do
- let :filename do
- 'skip.rb'
- end
-
- let :file_content do
- <<~EOS
- # -*- SkipSchemaAnnotations
- # == Schema Information
- #
- # Table name: foo
- #
- # id :integer not null, primary key
- # created_at :datetime
- # updated_at :datetime
- #
-
- class Foo < ActiveRecord::Base
- end
- EOS
- end
-
- let :expected_result do
- file_content
- end
-
- it 'does not remove annotation' do
- expect(file_content_after_removal).to eq expected_result
- end
- end
- end
-
- describe '.resolve_filename' do
- subject do
- AnnotateModels.resolve_filename(filename_template, model_name, table_name)
- end
-
- context 'When model_name is "example_model" and table_name is "example_models"' do
- let(:model_name) { 'example_model' }
- let(:table_name) { 'example_models' }
-
- context "when filename_template is 'test/unit/%MODEL_NAME%_test.rb'" do
- let(:filename_template) { 'test/unit/%MODEL_NAME%_test.rb' }
-
- it 'returns the test path for a model' do
- is_expected.to eq 'test/unit/example_model_test.rb'
- end
- end
-
- context "when filename_template is '/foo/bar/%MODEL_NAME%/testing.rb'" do
- let(:filename_template) { '/foo/bar/%MODEL_NAME%/testing.rb' }
-
- it 'returns the additional glob' do
- is_expected.to eq '/foo/bar/example_model/testing.rb'
- end
- end
-
- context "when filename_template is '/foo/bar/%PLURALIZED_MODEL_NAME%/testing.rb'" do
- let(:filename_template) { '/foo/bar/%PLURALIZED_MODEL_NAME%/testing.rb' }
-
- it 'returns the additional glob' do
- is_expected.to eq '/foo/bar/example_models/testing.rb'
- end
- end
-
- context "when filename_template is 'test/fixtures/%TABLE_NAME%.yml'" do
- let(:filename_template) { 'test/fixtures/%TABLE_NAME%.yml' }
-
- it 'returns the fixture path for a model' do
- is_expected.to eq 'test/fixtures/example_models.yml'
- end
- end
- end
-
- context 'When model_name is "parent/child" and table_name is "parent_children"' do
- let(:model_name) { 'parent/child' }
- let(:table_name) { 'parent_children' }
-
- context "when filename_template is 'test/fixtures/%PLURALIZED_MODEL_NAME%.yml'" do
- let(:filename_template) { 'test/fixtures/%PLURALIZED_MODEL_NAME%.yml' }
-
- it 'returns the fixture path for a nested model' do
- is_expected.to eq 'test/fixtures/parent/children.yml'
- end
- end
- end
- end
-
- describe 'annotating a file' do
+describe AnnotateModels do
+ describe 'when annotate a file' do
before do
@model_dir = Dir.mktmpdir('annotate_models')
(@model_file_name, @file_content) = write_model 'user.rb', <<~EOS
@@ -2724,7 +249,7 @@ class User < ActiveRecord::Base
end
end
- describe "if a file can't be annotated" do
+ describe "that can't be annotated" do
before do
allow(AnnotateModels).to receive(:get_loaded_model_by_path).with('user').and_return(nil)
@@ -2735,18 +260,18 @@ class User < ActiveRecord::Base
EOS
end
- it 'displays just the error message with trace disabled (default)' do
+ it 'should display just the error message with trace disabled (default)' do
expect { AnnotateModels.do_annotations model_dir: @model_dir, is_rake: true }.to output(a_string_including("Unable to annotate #{@model_dir}/user.rb: oops")).to_stderr
expect { AnnotateModels.do_annotations model_dir: @model_dir, is_rake: true }.not_to output(a_string_including('/spec/annotate/annotate_models_spec.rb:')).to_stderr
end
- it 'displays the error message and stacktrace with trace enabled' do
+ it 'should display the error message and stacktrace with trace enabled' do
expect { AnnotateModels.do_annotations model_dir: @model_dir, is_rake: true, trace: true }.to output(a_string_including("Unable to annotate #{@model_dir}/user.rb: oops")).to_stderr
expect { AnnotateModels.do_annotations model_dir: @model_dir, is_rake: true, trace: true }.to output(a_string_including('/spec/lib/annotate/annotate_models_spec.rb:')).to_stderr
end
end
- describe "if a file can't be deannotated" do
+ describe "that can't be de-annotated" do
before do
allow(AnnotateModels).to receive(:get_loaded_model_by_path).with('user').and_return(nil)
@@ -2757,12 +282,12 @@ class User < ActiveRecord::Base
EOS
end
- it 'displays just the error message with trace disabled (default)' do
+ it 'should display just the error message with trace disabled (default)' do
expect { AnnotateModels.remove_annotations model_dir: @model_dir, is_rake: true }.to output(a_string_including("Unable to deannotate #{@model_dir}/user.rb: oops")).to_stderr
expect { AnnotateModels.remove_annotations model_dir: @model_dir, is_rake: true }.not_to output(a_string_including("/user.rb:2:in `'")).to_stderr
end
- it 'displays the error message and stacktrace with trace enabled' do
+ it 'should display the error message and stacktrace with trace enabled' do
expect { AnnotateModels.remove_annotations model_dir: @model_dir, is_rake: true, trace: true }.to output(a_string_including("Unable to deannotate #{@model_dir}/user.rb: oops")).to_stderr
expect { AnnotateModels.remove_annotations model_dir: @model_dir, is_rake: true, trace: true }.to output(a_string_including("/user.rb:2:in `'")).to_stderr
end
diff --git a/spec/lib/annotate/annotate_models/file_patterns_spec.rb b/spec/lib/annotate/models/file_patterns_spec.rb
similarity index 100%
rename from spec/lib/annotate/annotate_models/file_patterns_spec.rb
rename to spec/lib/annotate/models/file_patterns_spec.rb
diff --git a/spec/lib/annotate/models/get_model_class_spec.rb b/spec/lib/annotate/models/get_model_class_spec.rb
new file mode 100644
index 000000000..53f83a074
--- /dev/null
+++ b/spec/lib/annotate/models/get_model_class_spec.rb
@@ -0,0 +1,408 @@
+require_relative '../../../spec_helper'
+require_relative 'model_spec_helper'
+require 'annotate/annotate_models'
+require 'annotate/active_record_patch'
+require 'active_support/core_ext/string'
+require 'files'
+require 'tmpdir'
+
+describe AnnotateModels do
+ describe '.get_model_class' do
+ before :all do
+ AnnotateModels.model_dir = Dir.mktmpdir('annotate_models')
+ end
+
+ # TODO: use 'files' gem instead
+ def create(filename, file_content)
+ File.join(AnnotateModels.model_dir[0], filename).tap do |path|
+ FileUtils.mkdir_p(File.dirname(path))
+ File.open(path, 'wb') do |f|
+ f.puts(file_content)
+ end
+ end
+ end
+
+ before :each do
+ create(filename, file_content)
+ end
+
+ let :klass do
+ AnnotateModels.get_model_class(File.join(AnnotateModels.model_dir[0], filename))
+ end
+
+ context 'when class Foo is defined in "foo.rb"' do
+ let :filename do
+ 'foo.rb'
+ end
+
+ let :file_content do
+ <<~EOS
+ class Foo < ActiveRecord::Base
+ end
+ EOS
+ end
+
+ it 'works' do
+ expect(klass.name).to eq('Foo')
+ end
+ end
+
+ context 'when class name is not capitalized normally' do
+ context 'when class FooWithCAPITALS is defined in "foo_with_capitals.rb"' do
+ let :filename do
+ 'foo_with_capitals.rb'
+ end
+
+ let :file_content do
+ <<~EOS
+ class FooWithCAPITALS < ActiveRecord::Base
+ end
+ EOS
+ end
+
+ it 'works' do
+ expect(klass.name).to eq('FooWithCAPITALS')
+ end
+ end
+ end
+
+ context 'when class is defined inside module' do
+ context 'when class Bar::FooInsideBar is defined in "bar/foo_inside_bar.rb"' do
+ let :filename do
+ 'bar/foo_inside_bar.rb'
+ end
+
+ let :file_content do
+ <<~EOS
+ module Bar
+ class FooInsideBar < ActiveRecord::Base
+ end
+ end
+ EOS
+ end
+
+ it 'works' do
+ expect(klass.name).to eq('Bar::FooInsideBar')
+ end
+ end
+ end
+
+ context 'when class is defined inside module and class name is not capitalized normally' do
+ context 'when class Bar::FooInsideCapitalsBAR is defined in "bar/foo_inside_capitals_bar.rb"' do
+ let :filename do
+ 'bar/foo_inside_capitals_bar.rb'
+ end
+
+ let :file_content do
+ <<~EOS
+ module BAR
+ class FooInsideCapitalsBAR < ActiveRecord::Base
+ end
+ end
+ EOS
+ end
+
+ it 'works' do
+ expect(klass.name).to eq('BAR::FooInsideCapitalsBAR')
+ end
+ end
+ end
+
+ context 'when unknown macros exist in class' do
+ context 'when class FooWithMacro is defined in "foo_with_macro.rb"' do
+ let :filename do
+ 'foo_with_macro.rb'
+ end
+
+ let :file_content do
+ <<~EOS
+ class FooWithMacro < ActiveRecord::Base
+ acts_as_awesome :yah
+ end
+ EOS
+ end
+
+ it 'works and does not care about known macros' do
+ expect(klass.name).to eq('FooWithMacro')
+ end
+ end
+
+ context 'when class name is with ALL CAPS segments' do
+ context 'when class is "FooWithCAPITALS" is defined in "foo_with_capitals.rb"' do
+ let :filename do
+ 'foo_with_capitals.rb'
+ end
+
+ let :file_content do
+ <<~EOS
+ class FooWithCAPITALS < ActiveRecord::Base
+ acts_as_awesome :yah
+ end
+ EOS
+ end
+
+ it 'works' do
+ expect(klass.name).to eq('FooWithCAPITALS')
+ end
+ end
+ end
+ end
+
+ context 'when known macros exist in class' do
+ context 'when class FooWithKnownMacro is defined in "foo_with_known_macro.rb"' do
+ let :filename do
+ 'foo_with_known_macro.rb'
+ end
+
+ let :file_content do
+ <<~EOS
+ class FooWithKnownMacro < ActiveRecord::Base
+ has_many :yah
+ end
+ EOS
+ end
+
+ it 'works and does not care about known macros' do
+ expect(klass.name).to eq('FooWithKnownMacro')
+ end
+ end
+ end
+
+ context 'when the file includes invalid multibyte chars (USASCII)' do
+ context 'when class FooWithUtf8 is defined in "foo_with_utf8.rb"' do
+ let :filename do
+ 'foo_with_utf8.rb'
+ end
+
+ let :file_content do
+ <<~EOS
+ # encoding: utf-8
+ class FooWithUtf8 < ActiveRecord::Base
+ UTF8STRINGS = %w[résumé façon âge]
+ end
+ EOS
+ end
+
+ it 'works without complaining of invalid multibyte chars' do
+ expect(klass.name).to eq('FooWithUtf8')
+ end
+ end
+ end
+
+ context 'when non-namespaced model is inside subdirectory' do
+ context 'when class NonNamespacedFooInsideBar is defined in "bar/non_namespaced_foo_inside_bar.rb"' do
+ let :filename do
+ 'bar/non_namespaced_foo_inside_bar.rb'
+ end
+
+ let :file_content do
+ <<~EOS
+ class NonNamespacedFooInsideBar < ActiveRecord::Base
+ end
+ EOS
+ end
+
+ it 'works' do
+ expect(klass.name).to eq('NonNamespacedFooInsideBar')
+ end
+ end
+
+ context 'when class name is not capitalized normally' do
+ context 'when class NonNamespacedFooWithCapitalsInsideBar is defined in "bar/non_namespaced_foo_with_capitals_inside_bar.rb"' do
+ let :filename do
+ 'bar/non_namespaced_foo_with_capitals_inside_bar.rb'
+ end
+
+ let :file_content do
+ <<~EOS
+ class NonNamespacedFooWithCapitalsInsideBar < ActiveRecord::Base
+ end
+ EOS
+ end
+
+ it 'works' do
+ expect(klass.name).to eq('NonNamespacedFooWithCapitalsInsideBar')
+ end
+ end
+ end
+ end
+
+ context 'when class file is loaded twice' do
+ context 'when class LoadedClass is defined in "loaded_class.rb"' do
+ let :filename do
+ 'loaded_class.rb'
+ end
+
+ let :file_content do
+ <<~EOS
+ class LoadedClass < ActiveRecord::Base
+ CONSTANT = 1
+ end
+ EOS
+ end
+
+ before :each do
+ path = File.expand_path(filename, AnnotateModels.model_dir[0])
+ Kernel.load(path)
+ expect(Kernel).not_to receive(:require)
+ end
+
+ it 'does not require model file twice' do
+ expect(klass.name).to eq('LoadedClass')
+ end
+ end
+
+ context 'when class is defined in a subdirectory' do
+ dir = Array.new(8) { (0..9).to_a.sample(random: Random.new) }.join
+
+ context "when class SubdirLoadedClass is defined in \"#{dir}/subdir_loaded_class.rb\"" do
+ before :each do
+ $LOAD_PATH.unshift(File.join(AnnotateModels.model_dir[0], dir))
+
+ path = File.expand_path(filename, AnnotateModels.model_dir[0])
+ Kernel.load(path)
+ expect(Kernel).not_to receive(:require)
+ end
+
+ let :filename do
+ "#{dir}/subdir_loaded_class.rb"
+ end
+
+ let :file_content do
+ <<~EOS
+ class SubdirLoadedClass < ActiveRecord::Base
+ CONSTANT = 1
+ end
+ EOS
+ end
+
+ it 'does not require model file twice' do
+ expect(klass.name).to eq('SubdirLoadedClass')
+ end
+ end
+ end
+ end
+
+ context 'when two class exist' do
+ before :each do
+ create(filename_2, file_content_2)
+ end
+
+ context 'the base names are duplicated' do
+ let :filename do
+ 'foo.rb'
+ end
+
+ let :file_content do
+ <<-EOS
+ class Foo < ActiveRecord::Base
+ end
+ EOS
+ end
+
+ let :filename_2 do
+ 'bar/foo.rb'
+ end
+
+ let :file_content_2 do
+ <<-EOS
+ class Bar::Foo < ActiveRecord::Base
+ end
+ EOS
+ end
+
+ let :klass_2 do
+ AnnotateModels.get_model_class(File.join(AnnotateModels.model_dir[0], filename_2))
+ end
+
+ it 'finds valid model' do
+ expect(klass.name).to eq('Foo')
+ expect(klass_2.name).to eq('Bar::Foo')
+ end
+ end
+
+ context 'the class name and base name clash' do
+ let :filename do
+ 'foo.rb'
+ end
+
+ let :file_content do
+ <<-EOS
+ class Foo < ActiveRecord::Base
+ end
+ EOS
+ end
+
+ let :filename_2 do
+ 'bar/foo.rb'
+ end
+
+ let :file_content_2 do
+ <<-EOS
+ class Bar::Foo < ActiveRecord::Base
+ end
+ EOS
+ end
+
+ let :klass_2 do
+ AnnotateModels.get_model_class(File.join(AnnotateModels.model_dir[0], filename_2))
+ end
+
+ it 'finds valid model' do
+ expect(klass.name).to eq('Foo')
+ expect(klass_2.name).to eq('Bar::Foo')
+ end
+
+ it 'attempts to load the model path without expanding if skip_subdirectory_model_load is false' do
+ allow(AnnotateModels).to receive(:skip_subdirectory_model_load).and_return(false)
+ full_path = File.join(AnnotateModels.model_dir[0], filename_2)
+ expect(File).to_not receive(:expand_path).with(full_path)
+ AnnotateModels.get_model_class(full_path)
+ end
+
+ it 'does not attempt to load the model path without expanding if skip_subdirectory_model_load is true' do
+ $LOAD_PATH.unshift(AnnotateModels.model_dir[0])
+ allow(AnnotateModels).to receive(:skip_subdirectory_model_load).and_return(true)
+ full_path = File.join(AnnotateModels.model_dir[0], filename_2)
+ expect(File).to receive(:expand_path).with(full_path).and_call_original
+ AnnotateModels.get_model_class(full_path)
+ end
+ end
+
+ context 'one of the classes is nested in another class' do
+ let :filename do
+ 'voucher.rb'
+ end
+
+ let :file_content do
+ <<-EOS
+ class Voucher < ActiveRecord::Base
+ end
+ EOS
+ end
+
+ let :filename_2 do
+ 'voucher/foo.rb'
+ end
+
+ let :file_content_2 do
+ <<~EOS
+ class Voucher
+ class Foo < ActiveRecord::Base
+ end
+ end
+ EOS
+ end
+
+ let :klass_2 do
+ AnnotateModels.get_model_class(File.join(AnnotateModels.model_dir[0], filename_2))
+ end
+
+ it 'finds valid model' do
+ expect(klass.name).to eq('Voucher')
+ expect(klass_2.name).to eq('Voucher::Foo')
+ end
+ end
+ end
+ end
+end
diff --git a/spec/lib/annotate/models/get_model_files_spec.rb b/spec/lib/annotate/models/get_model_files_spec.rb
new file mode 100644
index 000000000..66857fa1e
--- /dev/null
+++ b/spec/lib/annotate/models/get_model_files_spec.rb
@@ -0,0 +1,120 @@
+require_relative '../../../spec_helper'
+require_relative 'model_spec_helper'
+require 'annotate/annotate_models'
+require 'annotate/active_record_patch'
+require 'active_support/core_ext/string'
+require 'files'
+require 'tmpdir'
+
+describe AnnotateModels do
+ describe '.get_model_files' do
+ subject { described_class.get_model_files(options) }
+
+ before do
+ ARGV.clear
+
+ described_class.model_dir = [model_dir]
+ end
+
+ context 'when `model_dir` is valid' do
+ let(:model_dir) do
+ Files do
+ file 'foo.rb'
+ dir 'bar' do
+ file 'baz.rb'
+ dir 'qux' do
+ file 'quux.rb'
+ end
+ end
+ dir 'concerns' do
+ file 'corge.rb'
+ end
+ end
+ end
+
+ context 'when the model files are not specified' do
+ context 'when no option is specified' do
+ let(:options) { {} }
+
+ it 'returns all model files under `model_dir` directory' do
+ is_expected.to contain_exactly(
+ [model_dir, 'foo.rb'],
+ [model_dir, File.join('bar', 'baz.rb')],
+ [model_dir, File.join('bar', 'qux', 'quux.rb')]
+ )
+ end
+ end
+
+ context 'when `ignore_model_sub_dir` option is enabled' do
+ let(:options) { { ignore_model_sub_dir: true } }
+
+ it 'returns model files just below `model_dir` directory' do
+ is_expected.to contain_exactly([model_dir, 'foo.rb'])
+ end
+ end
+ end
+
+ context 'when the model files are specified' do
+ let(:additional_model_dir) { 'additional_model' }
+ let(:model_files) do
+ [
+ File.join(model_dir, 'foo.rb'),
+ "./#{File.join(additional_model_dir, 'corge/grault.rb')}" # Specification by relative path
+ ]
+ end
+
+ before { ARGV.concat(model_files) }
+
+ context 'when no option is specified' do
+ let(:options) { {} }
+
+ context 'when all the specified files are in `model_dir` directory' do
+ before do
+ described_class.model_dir << additional_model_dir
+ end
+
+ it 'returns specified files' do
+ is_expected.to contain_exactly(
+ [model_dir, 'foo.rb'],
+ [additional_model_dir, 'corge/grault.rb']
+ )
+ end
+ end
+
+ context 'when a model file outside `model_dir` directory is specified' do
+ it 'exits with the status code' do
+ subject
+ raise
+ rescue SystemExit => e
+ expect(e.status).to eq(1)
+ end
+ end
+ end
+
+ context 'when `is_rake` option is enabled' do
+ let(:options) { { is_rake: true } }
+
+ it 'returns all model files under `model_dir` directory' do
+ is_expected.to contain_exactly(
+ [model_dir, 'foo.rb'],
+ [model_dir, File.join('bar', 'baz.rb')],
+ [model_dir, File.join('bar', 'qux', 'quux.rb')]
+ )
+ end
+ end
+ end
+ end
+
+ context 'when `model_dir` is invalid' do
+ let(:model_dir) { '/not_exist_path' }
+ let(:options) { {} }
+
+ it 'exits with the status code' do
+ subject
+ raise
+ rescue SystemExit => e
+ expect(e.status).to eq(1)
+ end
+ end
+ end
+end
diff --git a/spec/lib/annotate/models/get_patterns_spec.rb b/spec/lib/annotate/models/get_patterns_spec.rb
new file mode 100644
index 000000000..602f93e2b
--- /dev/null
+++ b/spec/lib/annotate/models/get_patterns_spec.rb
@@ -0,0 +1,40 @@
+require_relative '../../../spec_helper'
+require_relative 'model_spec_helper'
+require 'annotate/annotate_models'
+require 'annotate/active_record_patch'
+require 'active_support/core_ext/string'
+require 'files'
+require 'tmpdir'
+
+describe AnnotateModels do
+ describe '.get_patterns' do
+ subject { AnnotateModels.get_patterns(options, pattern_type) }
+
+ context 'when pattern_type is "additional_file_patterns"' do
+ let(:pattern_type) { 'additional_file_patterns' }
+
+ context 'when additional_file_patterns is specified in the options' do
+ let(:additional_file_patterns) do
+ [
+ '/%PLURALIZED_MODEL_NAME%/**/*.rb',
+ '/bar/%PLURALIZED_MODEL_NAME%/*_form'
+ ]
+ end
+
+ let(:options) { { additional_file_patterns: additional_file_patterns } }
+
+ it 'returns additional_file_patterns in the argument "options"' do
+ is_expected.to eq(additional_file_patterns)
+ end
+ end
+
+ context 'when additional_file_patterns is not specified in the options' do
+ let(:options) { {} }
+
+ it 'returns an empty array' do
+ is_expected.to eq([])
+ end
+ end
+ end
+ end
+end
diff --git a/spec/lib/annotate/models/get_schema_info_spec.rb b/spec/lib/annotate/models/get_schema_info_spec.rb
new file mode 100644
index 000000000..577779ff2
--- /dev/null
+++ b/spec/lib/annotate/models/get_schema_info_spec.rb
@@ -0,0 +1,1436 @@
+require_relative '../../../spec_helper'
+require_relative 'model_spec_helper'
+require 'annotate/annotate_models'
+require 'annotate/active_record_patch'
+require 'active_support/core_ext/string'
+require 'files'
+require 'tmpdir'
+
+describe AnnotateModels do
+ 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
+end
diff --git a/spec/lib/annotate/models/model_spec_helper.rb b/spec/lib/annotate/models/model_spec_helper.rb
new file mode 100644
index 000000000..00f581e6e
--- /dev/null
+++ b/spec/lib/annotate/models/model_spec_helper.rb
@@ -0,0 +1,57 @@
+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
diff --git a/spec/lib/annotate/models/parse_options_spec.rb b/spec/lib/annotate/models/parse_options_spec.rb
new file mode 100644
index 000000000..bb1192bc5
--- /dev/null
+++ b/spec/lib/annotate/models/parse_options_spec.rb
@@ -0,0 +1,76 @@
+require_relative '../../../spec_helper'
+require 'annotate/annotate_models'
+require 'annotate/active_record_patch'
+require 'active_support/core_ext/string'
+require 'files'
+require 'tmpdir'
+
+describe AnnotateModels do
+ describe '.parse_options' do
+ let(:options) do
+ {
+ root_dir: '/root',
+ model_dir: 'app/models,app/one, app/two ,,app/three',
+ skip_subdirectory_model_load: false
+ }
+ end
+
+ before :each do
+ AnnotateModels.send(:parse_options, options)
+ end
+
+ describe '@root_dir' do
+ subject do
+ AnnotateModels.instance_variable_get(:@root_dir)
+ end
+
+ it 'sets @root_dir' do
+ is_expected.to eq('/root')
+ end
+ end
+
+ describe '@model_dir' do
+ subject do
+ AnnotateModels.instance_variable_get(:@model_dir)
+ end
+
+ it 'separates option "model_dir" with commas and sets @model_dir as an array of string' do
+ is_expected.to eq(['app/models', 'app/one', 'app/two', 'app/three'])
+ end
+ end
+
+ describe '@skip_subdirectory_model_load' do
+ subject do
+ AnnotateModels.instance_variable_get(:@skip_subdirectory_model_load)
+ end
+
+ context 'option is set to true' do
+ let(:options) do
+ {
+ root_dir: '/root',
+ model_dir: 'app/models,app/one, app/two ,,app/three',
+ skip_subdirectory_model_load: true
+ }
+ end
+
+ it 'sets skip_subdirectory_model_load to true' do
+ is_expected.to eq(true)
+ end
+ end
+
+ context 'option is set to false' do
+ let(:options) do
+ {
+ root_dir: '/root',
+ model_dir: 'app/models,app/one, app/two ,,app/three',
+ skip_subdirectory_model_load: false
+ }
+ end
+
+ it 'sets skip_subdirectory_model_load to false' do
+ is_expected.to eq(false)
+ end
+ end
+ end
+ end
+end
diff --git a/spec/lib/annotate/models/quote_spec.rb b/spec/lib/annotate/models/quote_spec.rb
new file mode 100644
index 000000000..1a30c05e7
--- /dev/null
+++ b/spec/lib/annotate/models/quote_spec.rb
@@ -0,0 +1,72 @@
+require_relative '../../../spec_helper'
+require 'annotate/annotate_models'
+require 'annotate/active_record_patch'
+require 'active_support/core_ext/string'
+require 'files'
+require 'tmpdir'
+
+describe AnnotateModels do
+ 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
+end
diff --git a/spec/lib/annotate/models/remove_annotation_of_file_spec.rb b/spec/lib/annotate/models/remove_annotation_of_file_spec.rb
new file mode 100644
index 000000000..d2f9ea4fc
--- /dev/null
+++ b/spec/lib/annotate/models/remove_annotation_of_file_spec.rb
@@ -0,0 +1,241 @@
+require_relative '../../../spec_helper'
+require_relative 'model_spec_helper'
+require 'annotate/annotate_models'
+require 'annotate/active_record_patch'
+require 'active_support/core_ext/string'
+require 'files'
+require 'tmpdir'
+
+describe AnnotateModels do
+ describe '.remove_annotation_of_file' do
+ subject do
+ AnnotateModels.remove_annotation_of_file(path)
+ end
+
+ let :tmpdir do
+ Dir.mktmpdir('annotate_models')
+ end
+
+ let :path do
+ File.join(tmpdir, filename).tap do |path|
+ File.open(path, 'w') do |f|
+ f.puts(file_content)
+ end
+ end
+ end
+
+ let :file_content_after_removal do
+ subject
+ File.read(path)
+ end
+
+ let :expected_result do
+ <<~EOS
+ class Foo < ActiveRecord::Base
+ end
+ EOS
+ end
+
+ context 'when annotation is before main content' do
+ let :filename do
+ 'before.rb'
+ end
+
+ let :file_content do
+ <<~EOS
+ # == Schema Information
+ #
+ # Table name: foo
+ #
+ # id :integer not null, primary key
+ # created_at :datetime
+ # updated_at :datetime
+ #
+
+ class Foo < ActiveRecord::Base
+ end
+ EOS
+ end
+
+ it 'removes annotation' do
+ expect(file_content_after_removal).to eq expected_result
+ end
+ end
+
+ context 'when annotation is before main content and CRLF is used for line breaks' do
+ let :filename do
+ 'before.rb'
+ end
+
+ let :file_content do
+ <<~EOS
+ # == Schema Information
+ #
+ # Table name: foo\r\n#
+ # id :integer not null, primary key
+ # created_at :datetime
+ # updated_at :datetime
+ #
+ \r\n
+ class Foo < ActiveRecord::Base
+ end
+ EOS
+ end
+
+ it 'removes annotation' do
+ expect(file_content_after_removal).to eq expected_result
+ end
+ end
+
+ context 'when annotation is before main content and with opening wrapper' do
+ let :filename do
+ 'opening_wrapper.rb'
+ end
+
+ let :file_content do
+ <<~EOS
+ # wrapper
+ # == Schema Information
+ #
+ # Table name: foo
+ #
+ # id :integer not null, primary key
+ # created_at :datetime
+ # updated_at :datetime
+ #
+
+ class Foo < ActiveRecord::Base
+ end
+ EOS
+ end
+
+ subject do
+ AnnotateModels.remove_annotation_of_file(path, wrapper_open: 'wrapper')
+ end
+
+ it 'removes annotation' do
+ expect(file_content_after_removal).to eq expected_result
+ end
+ end
+
+ context 'when annotation is before main content and with opening wrapper' do
+ let :filename do
+ 'opening_wrapper.rb'
+ end
+
+ let :file_content do
+ <<~EOS
+ # wrapper\r\n# == Schema Information
+ #
+ # Table name: foo
+ #
+ # id :integer not null, primary key
+ # created_at :datetime
+ # updated_at :datetime
+ #
+
+ class Foo < ActiveRecord::Base
+ end
+ EOS
+ end
+
+ subject do
+ AnnotateModels.remove_annotation_of_file(path, wrapper_open: 'wrapper')
+ end
+
+ it 'removes annotation' do
+ expect(file_content_after_removal).to eq expected_result
+ end
+ end
+
+ context 'when annotation is after main content' do
+ let :filename do
+ 'after.rb'
+ end
+
+ let :file_content do
+ <<~EOS
+ class Foo < ActiveRecord::Base
+ end
+
+ # == Schema Information
+ #
+ # Table name: foo
+ #
+ # id :integer not null, primary key
+ # created_at :datetime
+ # updated_at :datetime
+ #
+
+ EOS
+ end
+
+ it 'removes annotation' do
+ expect(file_content_after_removal).to eq expected_result
+ end
+ end
+
+ context 'when annotation is after main content and with closing wrapper' do
+ let :filename do
+ 'closing_wrapper.rb'
+ end
+
+ let :file_content do
+ <<~EOS
+ class Foo < ActiveRecord::Base
+ end
+
+ # == Schema Information
+ #
+ # Table name: foo
+ #
+ # id :integer not null, primary key
+ # created_at :datetime
+ # updated_at :datetime
+ #
+ # wrapper
+
+ EOS
+ end
+
+ subject do
+ AnnotateModels.remove_annotation_of_file(path, wrapper_close: 'wrapper')
+ end
+
+ it 'removes annotation' do
+ expect(file_content_after_removal).to eq expected_result
+ end
+ end
+
+ context 'when annotation is before main content and with comment "-*- SkipSchemaAnnotations"' do
+ let :filename do
+ 'skip.rb'
+ end
+
+ let :file_content do
+ <<~EOS
+ # -*- SkipSchemaAnnotations
+ # == Schema Information
+ #
+ # Table name: foo
+ #
+ # id :integer not null, primary key
+ # created_at :datetime
+ # updated_at :datetime
+ #
+
+ class Foo < ActiveRecord::Base
+ end
+ EOS
+ end
+
+ let :expected_result do
+ file_content
+ end
+
+ it 'does not remove annotation' do
+ expect(file_content_after_removal).to eq expected_result
+ end
+ end
+ end
+end
diff --git a/spec/lib/annotate/models/resolve_filename_spec.rb b/spec/lib/annotate/models/resolve_filename_spec.rb
new file mode 100644
index 000000000..0f42e258b
--- /dev/null
+++ b/spec/lib/annotate/models/resolve_filename_spec.rb
@@ -0,0 +1,65 @@
+require_relative '../../../spec_helper'
+require_relative 'model_spec_helper'
+require 'annotate/annotate_models'
+require 'annotate/active_record_patch'
+require 'active_support/core_ext/string'
+require 'files'
+require 'tmpdir'
+
+describe AnnotateModels do
+ describe '.resolve_filename' do
+ subject do
+ AnnotateModels.resolve_filename(filename_template, model_name, table_name)
+ end
+
+ context 'When model_name is "example_model" and table_name is "example_models"' do
+ let(:model_name) { 'example_model' }
+ let(:table_name) { 'example_models' }
+
+ context "when filename_template is 'test/unit/%MODEL_NAME%_test.rb'" do
+ let(:filename_template) { 'test/unit/%MODEL_NAME%_test.rb' }
+
+ it 'returns the test path for a model' do
+ is_expected.to eq 'test/unit/example_model_test.rb'
+ end
+ end
+
+ context "when filename_template is '/foo/bar/%MODEL_NAME%/testing.rb'" do
+ let(:filename_template) { '/foo/bar/%MODEL_NAME%/testing.rb' }
+
+ it 'returns the additional glob' do
+ is_expected.to eq '/foo/bar/example_model/testing.rb'
+ end
+ end
+
+ context "when filename_template is '/foo/bar/%PLURALIZED_MODEL_NAME%/testing.rb'" do
+ let(:filename_template) { '/foo/bar/%PLURALIZED_MODEL_NAME%/testing.rb' }
+
+ it 'returns the additional glob' do
+ is_expected.to eq '/foo/bar/example_models/testing.rb'
+ end
+ end
+
+ context "when filename_template is 'test/fixtures/%TABLE_NAME%.yml'" do
+ let(:filename_template) { 'test/fixtures/%TABLE_NAME%.yml' }
+
+ it 'returns the fixture path for a model' do
+ is_expected.to eq 'test/fixtures/example_models.yml'
+ end
+ end
+ end
+
+ context 'When model_name is "parent/child" and table_name is "parent_children"' do
+ let(:model_name) { 'parent/child' }
+ let(:table_name) { 'parent_children' }
+
+ context "when filename_template is 'test/fixtures/%PLURALIZED_MODEL_NAME%.yml'" do
+ let(:filename_template) { 'test/fixtures/%PLURALIZED_MODEL_NAME%.yml' }
+
+ it 'returns the fixture path for a nested model' do
+ is_expected.to eq 'test/fixtures/parent/children.yml'
+ end
+ end
+ end
+ end
+end
diff --git a/spec/lib/annotate/models/set_defaults_spec.rb b/spec/lib/annotate/models/set_defaults_spec.rb
new file mode 100644
index 000000000..91fe285f9
--- /dev/null
+++ b/spec/lib/annotate/models/set_defaults_spec.rb
@@ -0,0 +1,34 @@
+require_relative '../../../spec_helper'
+require 'annotate/annotate_models'
+require 'annotate/active_record_patch'
+require 'active_support/core_ext/string'
+require 'files'
+require 'tmpdir'
+
+describe AnnotateModels do
+ describe '.set_defaults' do
+ subject do
+ Annotate::Helpers.true?(ENV['show_complete_foreign_keys'])
+ end
+
+ context 'when default value of "show_complete_foreign_keys" is not set' do
+ it 'returns false' do
+ is_expected.to be(false)
+ end
+ end
+
+ context 'when default value of "show_complete_foreign_keys" is set' do
+ before do
+ Annotate.set_defaults('show_complete_foreign_keys' => 'true')
+ end
+
+ it 'returns true' do
+ is_expected.to be(true)
+ end
+ end
+
+ after :each do
+ ENV.delete('show_complete_foreign_keys')
+ end
+ end
+end
diff --git a/spec/lib/annotate/annotate_routes_spec.rb b/spec/lib/annotate/routes/routes_spec.rb
similarity index 97%
rename from spec/lib/annotate/annotate_routes_spec.rb
rename to spec/lib/annotate/routes/routes_spec.rb
index a0ed118cc..6a7232f4e 100644
--- a/spec/lib/annotate/annotate_routes_spec.rb
+++ b/spec/lib/annotate/routes/routes_spec.rb
@@ -1,6 +1,20 @@
-require_relative '../../spec_helper'
+require_relative '../../../spec_helper'
require 'annotate/annotate_routes'
+MAGIC_COMMENTS = [
+ '# encoding: UTF-8',
+ '# coding: UTF-8',
+ '# -*- coding: UTF-8 -*-',
+ '#encoding: utf-8',
+ '# encoding: utf-8',
+ '# -*- encoding : utf-8 -*-',
+ "# encoding: utf-8\n# frozen_string_literal: true",
+ "# frozen_string_literal: true\n# encoding: utf-8",
+ '# frozen_string_literal: true',
+ '#frozen_string_literal: false',
+ '# -*- frozen_string_literal : true -*-'
+].freeze
+
describe AnnotateRoutes do
ROUTE_FILE = 'config/routes.rb'.freeze
@@ -9,20 +23,6 @@
MESSAGE_NOT_FOUND = "#{ROUTE_FILE} could not be found.".freeze
MESSAGE_REMOVED = "Annotations were removed from #{ROUTE_FILE}.".freeze
- MAGIC_COMMENTS = [
- '# encoding: UTF-8',
- '# coding: UTF-8',
- '# -*- coding: UTF-8 -*-',
- '#encoding: utf-8',
- '# encoding: utf-8',
- '# -*- encoding : utf-8 -*-',
- "# encoding: utf-8\n# frozen_string_literal: true",
- "# frozen_string_literal: true\n# encoding: utf-8",
- '# frozen_string_literal: true',
- '#frozen_string_literal: false',
- '# -*- frozen_string_literal : true -*-'
- ].freeze
-
let :stubs do
{}
end
@@ -97,7 +97,7 @@
# ## Route Map
#
- # Prefix | Verb | URI Pattern | Controller#Action
+ # Prefix | Verb | URI Pattern | Controller#Action#{' '}
# --------- | ---------- | --------------- | --------------------
# myaction1 | GET | /url1(.:format) | mycontroller1#action
# myaction2 | POST | /url2(.:format) | mycontroller2#action
@@ -189,7 +189,7 @@
# ## Route Map
#
- # Prefix | Verb | URI Pattern | Controller#Action
+ # Prefix | Verb | URI Pattern | Controller#Action#{' '}
# --------- | ---------- | --------------- | --------------------
# myaction1 | GET | /url1(.:format) | mycontroller1#action
# myaction2 | POST | /url2(.:format) | mycontroller2#action