Skip to content

Commit 017301f

Browse files
authored
Update column pattern regex to incorporate special column comments (#23)
* Extracts `AnnotationDiffGenerator` out of `FileAnnotator` * Updates the column pattern regex based on ctran/annotate_models#946 * Adds some tests for `FileAnnotator`
1 parent 322ef87 commit 017301f

12 files changed

+420
-27
lines changed

lib/annotate_rb/model_annotator.rb

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,5 +26,7 @@ module ModelAnnotator
2626
autoload :RelatedFilesListBuilder, 'annotate_rb/model_annotator/related_files_list_builder'
2727
autoload :AnnotationDecider, 'annotate_rb/model_annotator/annotation_decider'
2828
autoload :FileAnnotatorInstruction, 'annotate_rb/model_annotator/file_annotator_instruction'
29+
autoload :AnnotationDiffGenerator, 'annotate_rb/model_annotator/annotation_diff_generator'
30+
autoload :AnnotationDiff, 'annotate_rb/model_annotator/annotation_diff'
2931
end
3032
end
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
# frozen_string_literal: true
2+
3+
module AnnotateRb
4+
module ModelAnnotator
5+
# Plain old Ruby object for holding the differences
6+
class AnnotationDiff
7+
attr_reader :current_columns, :new_columns
8+
9+
def initialize(current_columns, new_columns)
10+
@current_columns = current_columns.dup.freeze
11+
@new_columns = new_columns.dup.freeze
12+
end
13+
14+
def changed?
15+
@changed ||= @current_columns != @new_columns
16+
end
17+
end
18+
end
19+
end
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
# frozen_string_literal: true
2+
3+
module AnnotateRb
4+
module ModelAnnotator
5+
# Compares the current file content and new annotation block and generates the column annotation differences
6+
class AnnotationDiffGenerator
7+
HEADER_PATTERN = /(^# Table name:.*?\n(#.*[\r]?\n)*[\r]?)/.freeze
8+
COLUMN_PATTERN = /^#[\t ]+[\w\*\.`\[\]():]+[\t ]+.+$/.freeze
9+
10+
class << self
11+
def call(file_content, annotation_block)
12+
new(file_content, annotation_block).generate
13+
end
14+
end
15+
16+
# @param [String] file_content The current file content of the model file we intend to annotate
17+
# @param [String] annotation_block The annotation block we intend to write to the model file
18+
def initialize(file_content, annotation_block)
19+
@file_content = file_content
20+
@annotation_block = annotation_block
21+
end
22+
23+
def generate
24+
# Ignore the Schema version line because it changes with each migration
25+
current_annotations = @file_content.match(HEADER_PATTERN).to_s
26+
new_annotations = @annotation_block.match(HEADER_PATTERN).to_s
27+
28+
if current_annotations.present?
29+
current_annotation_columns = current_annotations.scan(COLUMN_PATTERN).sort
30+
else
31+
current_annotation_columns = []
32+
end
33+
34+
if new_annotations.present?
35+
new_annotation_columns = new_annotations.scan(COLUMN_PATTERN).sort
36+
else
37+
new_annotation_columns = []
38+
end
39+
40+
_result = AnnotationDiff.new(current_annotation_columns, new_annotation_columns)
41+
end
42+
end
43+
end
44+
end

lib/annotate_rb/model_annotator/file_annotator.rb

Lines changed: 3 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -24,18 +24,12 @@ def call_with_instructions(instruction)
2424
def call(file_name, info_block, position, options = {})
2525
return false unless File.exist?(file_name)
2626
old_content = File.read(file_name)
27-
return false if old_content =~ /#{Constants::SKIP_ANNOTATION_PREFIX}.*\n/
2827

29-
# Ignore the Schema version line because it changes with each migration
30-
header_pattern = /(^# Table name:.*?\n(#.*[\r]?\n)*[\r]?)/
31-
old_header = old_content.match(header_pattern).to_s
32-
new_header = info_block.match(header_pattern).to_s
28+
return false if old_content =~ /#{Constants::SKIP_ANNOTATION_PREFIX}.*\n/
3329

34-
column_pattern = /^#[\t ]+[\w\*\.`]+[\t ]+.+$/
35-
old_columns = old_header && old_header.scan(column_pattern).sort
36-
new_columns = new_header && new_header.scan(column_pattern).sort
30+
diff = AnnotationDiffGenerator.new(old_content, info_block).generate
3731

38-
return false if old_columns == new_columns && !options[:force]
32+
return false if !diff.changed? && !options[:force]
3933

4034
abort "AnnotateRb error. #{file_name} needs to be updated, but annotaterb was run with `--frozen`." if options[:frozen]
4135

spec/lib/annotate_rb/annotate_models/annotate_models_annotating_a_file_spec.rb

Lines changed: 9 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -26,8 +26,7 @@ class User < ActiveRecord::Base
2626

2727
it "should put annotation before class if :position == 'before'" do
2828
annotate_one_file position: position
29-
expect(File.read(@model_file_name))
30-
.to eq("#{@schema_info}#{@file_content}")
29+
expect(File.read(@model_file_name)).to eq("#{@schema_info}#{@file_content}")
3130
end
3231
end
3332

@@ -36,8 +35,7 @@ class User < ActiveRecord::Base
3635

3736
it "should put annotation before class if :position == :before" do
3837
annotate_one_file position: position
39-
expect(File.read(@model_file_name))
40-
.to eq("#{@schema_info}#{@file_content}")
38+
expect(File.read(@model_file_name)).to eq("#{@schema_info}#{@file_content}")
4139
end
4240
end
4341

@@ -46,8 +44,7 @@ class User < ActiveRecord::Base
4644

4745
it "should put annotation before class if :position == 'top'" do
4846
annotate_one_file position: position
49-
expect(File.read(@model_file_name))
50-
.to eq("#{@schema_info}#{@file_content}")
47+
expect(File.read(@model_file_name)).to eq("#{@schema_info}#{@file_content}")
5148
end
5249
end
5350

@@ -56,8 +53,7 @@ class User < ActiveRecord::Base
5653

5754
it "should put annotation before class if :position == :top" do
5855
annotate_one_file position: position
59-
expect(File.read(@model_file_name))
60-
.to eq("#{@schema_info}#{@file_content}")
56+
expect(File.read(@model_file_name)).to eq("#{@schema_info}#{@file_content}")
6157
end
6258
end
6359

@@ -66,8 +62,7 @@ class User < ActiveRecord::Base
6662

6763
it "should put annotation after class if position: 'after'" do
6864
annotate_one_file position: position
69-
expect(File.read(@model_file_name))
70-
.to eq("#{@file_content}\n#{@schema_info}")
65+
expect(File.read(@model_file_name)).to eq("#{@file_content}\n#{@schema_info}")
7166
end
7267
end
7368

@@ -76,8 +71,7 @@ class User < ActiveRecord::Base
7671

7772
it "should put annotation after class if position: :after" do
7873
annotate_one_file position: position
79-
expect(File.read(@model_file_name))
80-
.to eq("#{@file_content}\n#{@schema_info}")
74+
expect(File.read(@model_file_name)).to eq("#{@file_content}\n#{@schema_info}")
8175
end
8276
end
8377

@@ -86,8 +80,7 @@ class User < ActiveRecord::Base
8680

8781
it "should put annotation after class if position: 'bottom'" do
8882
annotate_one_file position: position
89-
expect(File.read(@model_file_name))
90-
.to eq("#{@file_content}\n#{@schema_info}")
83+
expect(File.read(@model_file_name)).to eq("#{@file_content}\n#{@schema_info}")
9184
end
9285
end
9386

@@ -96,15 +89,13 @@ class User < ActiveRecord::Base
9689

9790
it "should put annotation after class if position: :bottom" do
9891
annotate_one_file position: position
99-
expect(File.read(@model_file_name))
100-
.to eq("#{@file_content}\n#{@schema_info}")
92+
expect(File.read(@model_file_name)).to eq("#{@file_content}\n#{@schema_info}")
10193
end
10294
end
10395

10496
it 'should wrap annotation if wrapper is specified' do
10597
annotate_one_file wrapper_open: 'START', wrapper_close: 'END'
106-
expect(File.read(@model_file_name))
107-
.to eq("# START\n#{@schema_info}# END\n#{@file_content}")
98+
expect(File.read(@model_file_name)).to eq("# START\n#{@schema_info}# END\n#{@file_content}")
10899
end
109100

110101
describe 'with existing annotation' do
Lines changed: 146 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,146 @@
1+
# frozen_string_literal: true
2+
3+
RSpec.describe AnnotateRb::ModelAnnotator::AnnotationDiffGenerator do
4+
def test_columns_match_expected
5+
remove_whitespace = Proc.new { |str| str.delete(" \t\r\n") }
6+
7+
resulting_current_columns_data = subject.current_columns.map(&remove_whitespace)
8+
expected_current_columns_data = current_columns.map(&remove_whitespace)
9+
resulting_new_columns_data = subject.new_columns.map(&remove_whitespace)
10+
expected_new_columns_data = new_columns.map(&remove_whitespace)
11+
12+
expect(resulting_current_columns_data).to eq(expected_current_columns_data)
13+
expect(resulting_new_columns_data).to eq(expected_new_columns_data)
14+
end
15+
16+
describe '.call' do
17+
subject { described_class.call(file_content, annotation_block) }
18+
19+
context 'when model file does not have any annotations' do
20+
let(:file_content) do
21+
<<~FILE
22+
class User < ApplicationRecord
23+
end
24+
FILE
25+
end
26+
let(:annotation_block) do
27+
<<~ANNOTATION
28+
# == Schema Information
29+
#
30+
# Table name: users
31+
#
32+
# id :bigint not null, primary key
33+
#
34+
ANNOTATION
35+
end
36+
37+
let(:current_columns) do
38+
[]
39+
end
40+
let(:new_columns) do
41+
[
42+
"# id :bigint not null, primary key",
43+
"# Table name: users"
44+
]
45+
end
46+
47+
it 'returns an AnnotationDiff object with the expected old and new columns' do
48+
test_columns_match_expected
49+
50+
expect(subject.changed?).to eq(true)
51+
end
52+
end
53+
54+
context 'when model files has the latest annotations (does not need to be updated)' do
55+
let(:file_content) do
56+
<<~FILE
57+
# == Schema Information
58+
#
59+
# Table name: users
60+
#
61+
# id :bigint not null, primary key
62+
#
63+
class User < ApplicationRecord
64+
end
65+
FILE
66+
end
67+
let(:annotation_block) do
68+
<<~ANNOTATION
69+
# == Schema Information
70+
#
71+
# Table name: users
72+
#
73+
# id :bigint not null, primary key
74+
#
75+
ANNOTATION
76+
end
77+
78+
let(:current_columns) do
79+
[
80+
"# id :bigint not null, primary key",
81+
"# Table name: users"
82+
]
83+
end
84+
let(:new_columns) do
85+
[
86+
"# id :bigint not null, primary key",
87+
"# Table name: users"
88+
]
89+
end
90+
91+
it 'returns an AnnotationDiff object with the expected old and new columns' do
92+
test_columns_match_expected
93+
94+
expect(subject.changed?).to eq(false)
95+
end
96+
end
97+
98+
context 'when model file has existing annotations with column comments' do
99+
let(:annotation_block) do
100+
<<~SCHEMA
101+
# == Schema Information
102+
#
103+
# Table name: users
104+
#
105+
# id :bigint not null, primary key
106+
# name([sensitivity: medium]) :string(50) not null
107+
#
108+
SCHEMA
109+
end
110+
let(:file_content) do
111+
<<~FILE
112+
# == Schema Information
113+
#
114+
# Table name: users
115+
#
116+
# id :bigint not null, primary key
117+
# name([sensitivity: low]) :string(50) not null
118+
#
119+
class User < ApplicationRecord
120+
end
121+
FILE
122+
end
123+
124+
let(:current_columns) do
125+
[
126+
"# id :bigint not null, primary key",
127+
"# name([sensitivity: low]) :string(50) not null",
128+
"# Table name: users"
129+
]
130+
end
131+
let(:new_columns) do
132+
[
133+
"# id :bigint not null, primary key",
134+
"# name([sensitivity: medium]) :string(50) not null",
135+
"# Table name: users"
136+
]
137+
end
138+
139+
it 'returns an AnnotationDiff object with the expected old and new columns' do
140+
test_columns_match_expected
141+
142+
expect(subject.changed?).to eq(true)
143+
end
144+
end
145+
end
146+
end
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
# frozen_string_literal: true
2+
3+
RSpec.describe AnnotateRb::ModelAnnotator::AnnotationDiff do
4+
describe 'attributes' do
5+
subject { described_class.new(current_columns, new_columns) }
6+
let(:current_columns) { 'some current columns string' }
7+
let(:new_columns) { 'some new columns string' }
8+
9+
it 'returns the current columns' do
10+
expect(subject.current_columns).to eq(current_columns)
11+
end
12+
13+
it 'returns the new columns' do
14+
expect(subject.new_columns).to eq(new_columns)
15+
end
16+
end
17+
18+
describe '#changed?' do
19+
subject { described_class.new(current_columns, new_columns).changed? }
20+
21+
context 'when the current and new columns are the same' do
22+
let(:current_columns) { 'the same' }
23+
let(:new_columns) { 'the same' }
24+
25+
it { is_expected.to eq(false) }
26+
end
27+
28+
context 'when the current and new columns are different' do
29+
let(:current_columns) { 'the current' }
30+
let(:new_columns) { 'the new' }
31+
32+
it { is_expected.to eq(true) }
33+
end
34+
end
35+
end

0 commit comments

Comments
 (0)