Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fixes updating columns when using alternative formats #784

Open
wants to merge 26 commits into
base: develop
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
d5d55e8
Merge branch 'hotfix/v2.6.7'
ctran Mar 10, 2015
52ffefc
Merge branch 'release/v2.6.8'
ctran Mar 12, 2015
1de5bfb
Merge branch 'release/v2.6.9'
ctran May 22, 2015
0146ee7
Merge branch 'release/v2.6.10'
ctran May 22, 2015
7b33065
Runs annotate on 'rake db:rollback' too
May 22, 2015
1ee47e5
Merge pull request #249 from agoodno/run-on-rollback
ctran Jun 16, 2015
8f3aeb1
Revert "Runs annotate on 'rake db:rollback' too"
ctran Jun 16, 2015
d878a5b
Merge pull request #259 from ctran/revert-249-run-on-rollback
ctran Jun 16, 2015
0b05456
Merge branch 'release/v2.7.0'
ctran Dec 30, 2015
5f37a97
Merge branch 'release/v2.7.2'
ctran Jun 2, 2017
89bce57
Merge branch 'release/v2.7.3'
ctran Apr 21, 2018
0ef095d
Merge branch 'release/2.7.5'
drwl Apr 22, 2019
111640d
Merge branch 'develop'
drwl Jun 22, 2019
ecc5c9e
Fixes updating columns when using alternative formats
tmr08c Mar 22, 2020
5de6948
Merge remote-tracking branch 'upstream/master' into tmr08c-fix-updati…
tmr08c May 9, 2020
1cd5aa3
Merge remote-tracking branch 'upstream' into tmr08c-fix-updating-yard…
tmr08c May 9, 2020
b1a3973
Extracts logic for determining the column pattern.
tmr08c May 9, 2020
45581f5
Fixes updating column type for `rdoc` and `yard`
tmr08c May 9, 2020
f38287b
Fixes updating annotation for a change in type for markdown format
tmr08c May 9, 2020
5a8e84b
Updates column regex for bare case
tmr08c May 17, 2020
e73783c
Adds separate function for foreign key column regular expression
tmr08c May 17, 2020
8de5c60
Fix updating foreign key checks
tmr08c May 17, 2020
e98a62c
Renames and fixes foreign_key_column_pattern
tmr08c May 17, 2020
6aef748
Merge remote-tracking branch 'origin/develop' into tmr08c-fix-updatin…
tmr08c May 17, 2020
4255689
Updates comments for `AnnotateModels#foreign_key_column_pattern_for`
tmr08c May 17, 2020
b88f55f
Removes extranesous space from `annotate_models_spec`
tmr08c May 17, 2020
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
81 changes: 80 additions & 1 deletion lib/annotate/annotate_models.rb
Original file line number Diff line number Diff line change
Expand Up @@ -364,6 +364,10 @@ def get_foreign_key_info(klass, options = {})
# :force<Symbol>:: whether to update the file even if it doesn't seem to need it.
# :position_in_*<Symbol>:: where to place the annotated section in fixture or model file,
# :before, :top, :after or :bottom. Default is :before.
# :format_bare<Boolean>:: whether to format annotations using default, bare syntax
# :format_markdown<Boolean>:: whether to format annotations using Markdown syntax
# :format_rdoc<Boolean>:: whether to format annotations using RDoc syntax
# :format_yard<Boolean>:: whether to format annotations using Yard syntax
#
def annotate_one_file(file_name, info_block, position, options = {})
return false unless File.exist?(file_name)
Expand All @@ -375,7 +379,9 @@ def annotate_one_file(file_name, info_block, position, options = {})
old_header = old_content.match(header_pattern).to_s
new_header = info_block.match(header_pattern).to_s

column_pattern = /^#[\t ]+[\w\*\.`]+[\t ]+.+$/
annotation_format = format_from(options)
column_pattern =
/#{column_pattern_for(annotation_format)}|#{foreign_key_column_pattern_for(annotation_format)}/
old_columns = old_header && old_header.scan(column_pattern).sort
new_columns = new_header && new_header.scan(column_pattern).sort

Expand Down Expand Up @@ -908,6 +914,79 @@ def get_attributes(column, column_type, klass, options)

attrs
end

# Returns the regular expression for finding a column definition based on
# the format of the annotations being made
#
# == Returns:
# Regular expression
#
# @param [Symbol] symbol representation of the format being used for
# annotations
def column_pattern_for(format_type)
case format_type
when :markdown then /^#\s+\*{2}`\w+`\*{2}(?:\s+\|\s`.*`)*$/
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I went for stricter matches. I'm not sure if it would make sense to be a bit looser and do something like:

/^#\s+\*{2}`\w+.*$/

that would catch

# **column_name** ...

Currently, I'm checking for the fact there may be additional columns in the markdown table

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

After revisiting this PR, I'm not sure how I feel about this. I'm sure if we just grabbing all lines in the annotation section would end up being a problem.

The previous implementation seemed to sort of be checking that the comment had an indication that it was the "column section" (e.g., # col_name or # * col_name). This PR goes a bit further and does some stricter checking based on the format.

If we pulled in any non-empty comment line, it seems it shouldn't cause a problem since we are essentially checking for any difference. If we pulled in non-column comments, we'd get them in both old_columns and new_columns, so it shouldn't be flagged as a change.

I think the situation that we are avoiding by doing it this way is if there are non-annotation comments in the annotation block since it looks like the header_pattern grabs all groups comments after the Table name: comment.

when :rdoc then
field_name_regex = /^#\s+\*\w+\*\:{2}/
field_info_regex = /\s+\<tt\>.*\<\/tt\>$/
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Similar to above, rather than checking the <tt>...</tt> this could all probably be .*$


/#{field_name_regex}#{field_info_regex}/
when :yard
attribute_regex = /^#\s@!attribute\s\w+$/
return_regex = /^#\s+@return\s+\[\w+\]$/

/#{attribute_regex}|#{return_regex}/
else # :bare/default
/^#\s+\w+\s?:\w+.*$/
end
end

# == Returns:
# Regular expression
#
# Checks for annotation comments that could represent a foreign key
# constraint. Handles the different formats that are supported. Also
# handles the optiona to shorten foreign keys with "...".
#
# @note Foreign key formatting only varies for markdown formatting. All
# other formats use the "bare" format.
#
# @param [Symbol] symbol representation of the format being used for
# annotations
def foreign_key_column_pattern_for(format_type)
case format_type
when :markdown
/^#\s+\*\s`[\w_]+(?:\.{3})?`\s.*$/
else
/^#\s+[\w_]+(?:\.{3})?\s+.*/
end
end

# Determine the current format being used for making annotations based on the options.
#
# == Returns:
# Symbol representing the format being used. Options include:
# :markdown
# :rdoc
# :yard
# :bare - this is the default option
#
# === Options (opts)
# :format_bare<Boolean>:: whether to format annotations using default, bare syntax
# :format_markdown<Boolean>:: whether to format annotations using Markdown syntax
# :format_rdoc<Boolean>:: whether to format annotations using RDoc syntax
# :format_yard<Boolean>:: whether to format annotations using Yard syntax
def format_from(options)
if options[:format_markdown]
:markdown
elsif options[:format_rdoc]
:rdoc
elsif options[:format_yard]
:yard
else
:bare
end
end
end

class BadModelFileError < LoadError
Expand Down
273 changes: 242 additions & 31 deletions spec/lib/annotate/annotate_models_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -2526,44 +2526,255 @@ def annotate_one_file(options = {})

describe 'with existing annotation' do
context 'of a foreign key' do
let(:class_name) { :users }
let(:primary_key) { :id }
let(:original_columns) do
[
mock_column(:id, :integer),
mock_column(:foreign_thing_id, :integer)
]
end
let(:original_foreign_keys) do
[
mock_foreign_key('fk_rails_cf2568e89e',
'foreign_thing_id',
'foreign_things',
'id',
on_delete: :cascade)
]
end

let(:options) do
{ show_foreign_keys: true }.merge(format_option)
end

before do
klass = mock_class(:users,
:id,
[
mock_column(:id, :integer),
mock_column(:foreign_thing_id, :integer)
],
klass = mock_class(class_name,
primary_key,
original_columns,
[],
[
mock_foreign_key('fk_rails_cf2568e89e',
'foreign_thing_id',
'foreign_things',
'id',
on_delete: :cascade)
])
@schema_info = AnnotateModels.get_schema_info(klass, '== Schema Info', show_foreign_keys: true)
original_foreign_keys)
@schema_info = AnnotateModels.get_schema_info(klass, '== Schema Info', options)
annotate_one_file
end

it 'should update foreign key constraint' do
klass = mock_class(:users,
:id,
[
mock_column(:id, :integer),
mock_column(:foreign_thing_id, :integer)
],
[],
[
mock_foreign_key('fk_rails_cf2568e89e',
'foreign_thing_id',
'foreign_things',
'id',
on_delete: :restrict)
])
@schema_info = AnnotateModels.get_schema_info(klass, '== Schema Info', show_foreign_keys: true)
annotate_one_file
context 'updating a constraint' do
let(:updated_foreign_keys) do
[
mock_foreign_key('fk_rails_cf2568e89e',
'foreign_thing_id',
'foreign_things',
'id',
on_delete: :restrict)
]
end

context 'when option "format_bare" is true' do
let(:format_option) { { format_bare: true } }

it 'should update foreign key constraint' do
klass = mock_class(class_name,
primary_key,
original_columns,
[],
updated_foreign_keys)

@schema_info = AnnotateModels.get_schema_info(klass, '== Schema Info', options)
annotate_one_file(options)

expect(File.read(@model_file_name)).to eq("#{@schema_info}#{@file_content}")
end
end

context 'when option "format_markdown" is true' do
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There didn't appear to be a test for the markdown format, so this adds a test for format_markdown and extracts some of the shared set up into befores and lets.

let(:format_option) { { format_markdown: true } }

it 'should update foreign key constraint' do
klass = mock_class(class_name,
primary_key,
original_columns,
[],
updated_foreign_keys)

@schema_info = AnnotateModels.get_schema_info(klass, '== Schema Info', options)
annotate_one_file(options)

expect(File.read(@model_file_name)).to eq("#{@schema_info}#{@file_content}")
end
end
end
end

context 'adding a new field' do
let(:class_name) { :users }
let(:primary_key) { :id }
let(:original_columns) do
[
mock_column(primary_key, :integer),
mock_column(:name, :string)
]
end

before do
klass = mock_class(class_name, primary_key, original_columns)
@schema_info = AnnotateModels.get_schema_info(klass, '== Schema Info', options)
annotate_one_file(options)

# confirm we initialized annotaions in file before checking for changes
expect(@schema_info).not_to be_empty
expect(File.read(@model_file_name)).to eq("#{@schema_info}#{@file_content}")
end

context 'when option "format_bare" is true' do
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I started going down the shared examples route to reduce the noise here. This snowballed a bit since there are additional places where checking for all formats would be useful. I have a WIP branch that I'd like to pick up if it looks like the right direction.

This PR ended up being bigger than I expected, so I thought it would be best to separate additional changes and conversations.

let :options do
{ format_bare: true }
end

it 'updates the fields list to include the new column' do
new_column_list = original_columns + [mock_column(:new_column, :string)]
klass = mock_class(class_name, primary_key, new_column_list)
@schema_info = AnnotateModels.get_schema_info(klass, '== Schema Info', options)
annotate_one_file(options)

expect(File.read(@model_file_name)).to eq("#{@schema_info}#{@file_content}")
end
end

context 'when option "format_yard" is true' do
let :options do
{ format_yard: true }
end

it 'updates the fields list to include the new column' do
new_column_list = original_columns + [mock_column(:new_column, :string)]
klass = mock_class(class_name, primary_key, new_column_list)
@schema_info = AnnotateModels.get_schema_info(klass, '== Schema Info', options)
annotate_one_file(options)

expect(File.read(@model_file_name)).to eq("#{@schema_info}#{@file_content}")
end
end

context 'when option "format_rdoc" is true' do
let :options do
{ format_rdoc: true }
end

it 'updates the fields list to include the new column' do
new_column_list = original_columns + [mock_column(:new_column, :string)]
klass = mock_class(class_name, primary_key, new_column_list)
@schema_info = AnnotateModels.get_schema_info(klass, '== Schema Info', options)
annotate_one_file(options)

expect(File.read(@model_file_name)).to eq("#{@schema_info}#{@file_content}")
end
end

context 'when option "format_markdown" is true' do
let :options do
{ format_markdown: true }
end

it 'updates the fields list to include the new column' do
# The new column name must be shorter than the existing columns
# becuase markdown formatting adds additional spacing. If the
# column name is long, the header row has space added triggering a
# difference even if we don't properly check the columns. By having
# a shorter column name we are testing our column list comparison
# and not an unintentional resizing.
new_column_list = original_columns + [mock_column(:a, :string)]
klass = mock_class(class_name, primary_key, new_column_list)
@schema_info = AnnotateModels.get_schema_info(klass, '== Schema Info', options)
annotate_one_file(options)

expect(File.read(@model_file_name)).to eq("#{@schema_info}#{@file_content}")
end
end
end

context 'changing a field type' do
let(:class_name) { :users }
let(:primary_key) { :id }
let(:original_columns) do
[
mock_column(primary_key, :integer),
mock_column(:some_field, :string)
]
end

# update `name` from `:string` to `:text`
let(:new_column_list) do
[
mock_column(primary_key, :integer),
mock_column(:some_field, :integer)
]
end

before do
klass = mock_class(class_name, primary_key, original_columns)
@schema_info = AnnotateModels.get_schema_info(klass, '== Schema Info', options)
annotate_one_file(options)

# confirm we initialized annotaions in file before checking for changes
expect(@schema_info).not_to be_empty
expect(File.read(@model_file_name)).to eq("#{@schema_info}#{@file_content}")
end

context 'when option "format_bare" is true' do
let :options do
{ format_bare: true }
end

it 'updates the fields list to include the new column' do
klass = mock_class(class_name, primary_key, new_column_list)
@schema_info = AnnotateModels.get_schema_info(klass, '== Schema Info', options)
annotate_one_file(options)

expect(File.read(@model_file_name)).to eq("#{@schema_info}#{@file_content}")
end
end

context 'when option "format_yard" is true' do
let :options do
{ format_yard: true }
end

it 'updates the fields list to include the new column' do
klass = mock_class(class_name, primary_key, new_column_list)
@schema_info = AnnotateModels.get_schema_info(klass, '== Schema Info', options)
annotate_one_file(options)

expect(File.read(@model_file_name)).to eq("#{@schema_info}#{@file_content}")
end
end

context 'when option "format_rdoc" is true' do
let :options do
{ format_rdoc: true }
end

it 'updates the fields list to include the new column' do
klass = mock_class(class_name, primary_key, new_column_list)
@schema_info = AnnotateModels.get_schema_info(klass, '== Schema Info', options)
annotate_one_file(options)

expect(File.read(@model_file_name)).to eq("#{@schema_info}#{@file_content}")
end
end

context 'when option "format_markdown" is true' do
let :options do
{ format_markdown: true }
end

it 'updates the fields list to include the new column' do
klass = mock_class(class_name, primary_key, new_column_list)
@schema_info = AnnotateModels.get_schema_info(klass, '== Schema Info', options)
annotate_one_file(options)

expect(File.read(@model_file_name)).to eq("#{@schema_info}#{@file_content}")
end
end
end
end

Expand Down