-
Notifications
You must be signed in to change notification settings - Fork 609
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
base: develop
Are you sure you want to change the base?
Changes from all commits
d5d55e8
52ffefc
1de5bfb
0146ee7
7b33065
1ee47e5
8f3aeb1
d878a5b
0b05456
5f37a97
89bce57
0ef095d
111640d
ecc5c9e
5de6948
1cd5aa3
b1a3973
45581f5
f38287b
5a8e84b
e73783c
8de5c60
e98a62c
6aef748
4255689
b88f55f
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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) | ||
|
@@ -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 | ||
|
||
|
@@ -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`.*`)*$/ | ||
when :rdoc then | ||
field_name_regex = /^#\s+\*\w+\*\:{2}/ | ||
field_info_regex = /\s+\<tt\>.*\<\/tt\>$/ | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Similar to above, rather than checking the |
||
|
||
/#{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 | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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 | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 |
||
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 | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 | ||
|
||
|
There was a problem hiding this comment.
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:
that would catch
Currently, I'm checking for the fact there may be additional columns in the markdown table
There was a problem hiding this comment.
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
andnew_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 theTable name:
comment.