Skip to content

Commit fdd843f

Browse files
authored
Refactor ModelAnnotator (#21)
* Refactors `ModelAnnotator` internals to (hopefully) improve maintainability * Fixes bugs in old Annotate project that looked at wrong option keys for `excludes_*`
1 parent ce338bf commit fdd843f

16 files changed

+645
-216
lines changed

lib/annotate_rb/model_annotator.rb

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,5 +23,8 @@ module ModelAnnotator
2323
autoload :ColumnAnnotationBuilder, 'annotate_rb/model_annotator/column_annotation_builder'
2424
autoload :IndexAnnotationBuilder, 'annotate_rb/model_annotator/index_annotation_builder'
2525
autoload :ForeignKeyAnnotationBuilder, 'annotate_rb/model_annotator/foreign_key_annotation_builder'
26+
autoload :RelatedFilesListBuilder, 'annotate_rb/model_annotator/related_files_list_builder'
27+
autoload :AnnotationDecider, 'annotate_rb/model_annotator/annotation_decider'
28+
autoload :FileAnnotatorInstruction, 'annotate_rb/model_annotator/file_annotator_instruction'
2629
end
2730
end
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
# frozen_string_literal: true
2+
3+
module AnnotateRb
4+
module ModelAnnotator
5+
# Class that encapsulates the logic to decide whether to annotate a model file and its related files or not.
6+
class AnnotationDecider
7+
def initialize(file, options)
8+
@file = file
9+
@options = options
10+
end
11+
12+
def annotate?
13+
return false if file_contains_skip_annotation
14+
15+
begin
16+
klass = ModelClassGetter.call(@file, @options)
17+
18+
klass_is_a_class = klass.is_a?(Class)
19+
klass_inherits_active_record_base = klass < ActiveRecord::Base
20+
klass_is_not_abstract = klass.respond_to?(:abstract_class) && !klass.abstract_class?
21+
klass_table_exists = klass.respond_to?(:abstract_class) && klass.table_exists?
22+
23+
not_sure_this_conditional = (!@options[:exclude_sti_subclasses] || !(klass.superclass < ActiveRecord::Base && klass.table_name == klass.superclass.table_name))
24+
25+
annotate_conditions = [
26+
klass_is_a_class,
27+
klass_inherits_active_record_base,
28+
not_sure_this_conditional,
29+
klass_is_not_abstract,
30+
klass_table_exists
31+
]
32+
33+
to_annotate = annotate_conditions.all?
34+
35+
return to_annotate
36+
rescue BadModelFileError => e
37+
unless @options[:ignore_unknown_models]
38+
$stderr.puts "Unable to annotate #{@file}: #{e.message}"
39+
$stderr.puts "\t" + e.backtrace.join("\n\t") if @options[:trace]
40+
end
41+
rescue StandardError => e
42+
$stderr.puts "Unable to annotate #{@file}: #{e.message}"
43+
$stderr.puts "\t" + e.backtrace.join("\n\t") if @options[:trace]
44+
end
45+
46+
false
47+
end
48+
49+
private
50+
51+
def file_contains_skip_annotation
52+
file_string = File.exist?(@file) ? File.read(@file) : ''
53+
54+
if /#{Constants::SKIP_ANNOTATION_PREFIX}.*/ =~ file_string
55+
true
56+
else
57+
false
58+
end
59+
end
60+
end
61+
end
62+
end

lib/annotate_rb/model_annotator/annotation_generator.rb

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -12,10 +12,9 @@ class AnnotationGenerator
1212
MD_NAMES_OVERHEAD = 6
1313
MD_TYPE_ALLOWANCE = 18
1414

15-
def initialize(klass, header, options = {})
16-
@header = header
17-
@options = options
15+
def initialize(klass, options = {})
1816
@model = ModelWrapper.new(klass, options)
17+
@options = options
1918
@info = "" # TODO: Make array and build string that way
2019
end
2120

@@ -50,9 +49,15 @@ def generate
5049
@info
5150
end
5251

53-
# TODO: Move header logic into here from AnnotateRb::ModelAnnotator::Annotator.do_annotations
5452
def header
55-
@header
53+
header = @options[:format_markdown] ? PREFIX_MD.dup : PREFIX.dup
54+
version = ActiveRecord::Migrator.current_version rescue 0
55+
56+
if @options[:include_version] && version > 0
57+
header += "\n# Schema version: #{version}"
58+
end
59+
60+
header
5661
end
5762

5863
def schema_header_text

lib/annotate_rb/model_annotator/annotator.rb

Lines changed: 27 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -1,31 +1,20 @@
1-
# require 'bigdecimal'
1+
# frozen_string_literal: true
22

33
module AnnotateRb
44
module ModelAnnotator
55
class Annotator
6-
# Annotate Models plugin use this header
7-
PREFIX = '== Schema Information'.freeze
8-
PREFIX_MD = '## Schema Information'.freeze
9-
10-
MAGIC_COMMENT_MATCHER = Regexp.new(/(^#\s*encoding:.*(?:\n|r\n))|(^# coding:.*(?:\n|\r\n))|(^# -\*- coding:.*(?:\n|\r\n))|(^# -\*- encoding\s?:.*(?:\n|\r\n))|(^#\s*frozen_string_literal:.+(?:\n|\r\n))|(^# -\*- frozen_string_literal\s*:.+-\*-(?:\n|\r\n))/).freeze
11-
126
class << self
13-
# We're passed a name of things that might be
14-
# ActiveRecord models. If we can find the class, and
15-
# if its a subclass of ActiveRecord::Base,
16-
# then pass it to the associated block
177
def do_annotations(options = {})
18-
header = options[:format_markdown] ? PREFIX_MD.dup : PREFIX.dup
19-
version = ActiveRecord::Migrator.current_version rescue 0
20-
if options[:include_version] && version > 0
21-
header << "\n# Schema version: #{version}"
22-
end
23-
248
annotated = []
25-
model_files_to_annotate = ModelFilesGetter.call(options)
269

27-
model_files_to_annotate.each do |path, filename|
28-
ModelFileAnnotator.call(annotated, File.join(path, filename), header, options)
10+
model_files_to_consider = ModelFilesGetter.call(options)
11+
12+
model_files_to_consider.each do |path, filename|
13+
file = File.join(path, filename)
14+
15+
if AnnotationDecider.new(file, options).annotate?
16+
ModelFileAnnotator.call(annotated, file, options)
17+
end
2918
end
3019

3120
if annotated.empty?
@@ -37,34 +26,41 @@ def do_annotations(options = {})
3726

3827
def remove_annotations(options = {})
3928
deannotated = []
40-
deannotated_klass = false
41-
ModelFilesGetter.call(options).each do |file|
42-
file = File.join(file)
29+
30+
model_files_to_consider = ModelFilesGetter.call(options)
31+
32+
model_files_to_consider.each do |path, filename|
33+
deannotated_klass = false
34+
file = File.join(path, filename)
35+
4336
begin
4437
klass = ModelClassGetter.call(file, options)
4538
if klass < ActiveRecord::Base && !klass.abstract_class?
4639
model_name = klass.name.underscore
4740
table_name = klass.table_name
48-
model_file_name = file
49-
deannotated_klass = true if FileAnnotationRemover.call(model_file_name, options)
5041

51-
patterns = PatternGetter.call(options)
42+
if FileAnnotationRemover.call(file, options)
43+
deannotated_klass = true
44+
end
45+
46+
related_files = RelatedFilesListBuilder.new(file, model_name, table_name, options).build
5247

53-
patterns
54-
.map { |f| FileNameResolver.call(f, model_name, table_name) }
55-
.each do |f|
48+
related_files.each do |f, _position_key|
5649
if File.exist?(f)
5750
FileAnnotationRemover.call(f, options)
58-
deannotated_klass = true
5951
end
6052
end
6153
end
62-
deannotated << klass if deannotated_klass
54+
55+
if deannotated_klass
56+
deannotated << klass
57+
end
6358
rescue StandardError => e
6459
$stderr.puts "Unable to deannotate #{File.join(file)}: #{e.message}"
6560
$stderr.puts "\t" + e.backtrace.join("\n\t") if options[:trace]
6661
end
6762
end
63+
6864
puts "Removed annotations from: #{deannotated.join(', ')}"
6965
end
7066
end

lib/annotate_rb/model_annotator/constants.rb

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,6 @@
11
module AnnotateRb
22
module ModelAnnotator
33
module Constants
4-
TRUE_RE = /^(true|t|yes|y|1)$/i.freeze
5-
64
##
75
# The set of available options to customize the behavior of Annotate.
86
#
@@ -17,6 +15,8 @@ module Constants
1715
ALL_ANNOTATE_OPTIONS = ::AnnotateRb::Options::ALL_OPTION_KEYS
1816

1917
SKIP_ANNOTATION_PREFIX = '# -\*- SkipSchemaAnnotations'.freeze
18+
19+
MAGIC_COMMENT_MATCHER = Regexp.new(/(^#\s*encoding:.*(?:\n|r\n))|(^# coding:.*(?:\n|\r\n))|(^# -\*- coding:.*(?:\n|\r\n))|(^# -\*- encoding\s?:.*(?:\n|\r\n))|(^#\s*frozen_string_literal:.+(?:\n|\r\n))|(^# -\*- frozen_string_literal\s*:.+-\*-(?:\n|\r\n))/).freeze
2020
end
2121
end
2222
end

lib/annotate_rb/model_annotator/file_annotator.rb

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,10 @@ module AnnotateRb
44
module ModelAnnotator
55
class FileAnnotator
66
class << self
7+
def call_with_instructions(instruction)
8+
call(instruction.file, instruction.annotation, instruction.position, instruction.options)
9+
end
10+
711
# Add a schema block to a file. If the file already contains
812
# a schema info block (a comment starting with "== Schema Information"),
913
# check if it matches the block that is already there. If so, leave it be.
@@ -33,7 +37,7 @@ def call(file_name, info_block, position, options = {})
3337

3438
return false if old_columns == new_columns && !options[:force]
3539

36-
abort "annotate error. #{file_name} needs to be updated, but annotate was run with `--frozen`." if options[:frozen]
40+
abort "AnnotateRb error. #{file_name} needs to be updated, but annotaterb was run with `--frozen`." if options[:frozen]
3741

3842
# Replace inline the old schema info with the new schema info
3943
wrapper_open = options[:wrapper_open] ? "# #{options[:wrapper_open]}\n" : ""
@@ -47,7 +51,7 @@ def call(file_name, info_block, position, options = {})
4751
# need to insert it in correct position
4852
if old_annotation.empty? || options[:force]
4953
magic_comments_block = Helper.magic_comments_as_string(old_content)
50-
old_content.gsub!(Annotator::MAGIC_COMMENT_MATCHER, '')
54+
old_content.gsub!(Constants::MAGIC_COMMENT_MATCHER, '')
5155

5256
annotation_pattern = AnnotationPatternGenerator.call(options)
5357
old_content.sub!(annotation_pattern, '')
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
# frozen_string_literal: true
2+
3+
module AnnotateRb
4+
module ModelAnnotator
5+
# A plain old Ruby object (PORO) that contains all necessary information for FileAnnotator
6+
class FileAnnotatorInstruction
7+
def initialize(file, annotation, position, options = {})
8+
@file = file # Path to file
9+
@annotation = annotation # Annotation string
10+
@position = position # Position in the file where to write the annotation to
11+
@options = options
12+
end
13+
14+
attr_reader :file, :annotation, :position, :options
15+
end
16+
end
17+
end

lib/annotate_rb/model_annotator/helper.rb

Lines changed: 1 addition & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,6 @@
33
module AnnotateRb
44
module ModelAnnotator
55
module Helper
6-
MATCHED_TYPES = %w(test fixture factory serializer scaffold controller helper).freeze
7-
86
INDEX_CLAUSES = {
97
unique: {
108
default: 'UNIQUE',
@@ -89,16 +87,8 @@ def width(string)
8987
string.chars.inject(0) { |acc, elem| acc + (elem.bytesize == 3 ? 2 : 1) }
9088
end
9189

92-
def matched_types(options)
93-
types = MATCHED_TYPES.dup
94-
types << 'admin' if options[:active_admin] =~ Constants::TRUE_RE && !types.include?('admin')
95-
types << 'additional_file_patterns' if options[:additional_file_patterns].present?
96-
97-
types
98-
end
99-
10090
def magic_comments_as_string(content)
101-
magic_comments = content.scan(Annotator::MAGIC_COMMENT_MATCHER).flatten.compact
91+
magic_comments = content.scan(Constants::MAGIC_COMMENT_MATCHER).flatten.compact
10292

10393
if magic_comments.any?
10494
magic_comments.join
@@ -107,10 +97,6 @@ def magic_comments_as_string(content)
10797
end
10898
end
10999

110-
def true?(val)
111-
val.present? && Constants::TRUE_RE.match?(val)
112-
end
113-
114100
# TODO: Find another implementation that doesn't depend on ActiveSupport
115101
def fallback(*args)
116102
args.compact.detect(&:present?)

0 commit comments

Comments
 (0)