Skip to content

Commit 2472836

Browse files
authored
Refactor ModelAnnotator again (#28)
* Refactors a lot of the internals of `ModelAnnotator` * Add structure to the annotation builder and its components * Makes the logic for adding and removing annotations near symmetrical
1 parent 1ce7d6d commit 2472836

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

53 files changed

+1571
-1212
lines changed

lib/annotate_rb.rb

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212

1313
require 'annotate_rb/active_record_patch'
1414

15+
require_relative 'annotate_rb/helper'
1516
require_relative 'annotate_rb/core'
1617
require_relative 'annotate_rb/commands'
1718
require_relative 'annotate_rb/parser'

lib/annotate_rb/helper.rb

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
# frozen_string_literal: true
2+
3+
module AnnotateRb
4+
module Helper
5+
class << self
6+
def width(string)
7+
string.chars.inject(0) { |acc, elem| acc + (elem.bytesize == 3 ? 2 : 1) }
8+
end
9+
10+
# TODO: Find another implementation that doesn't depend on ActiveSupport
11+
def fallback(*args)
12+
args.compact.detect(&:present?)
13+
end
14+
end
15+
end
16+
end

lib/annotate_rb/model_annotator.rb

Lines changed: 13 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -3,30 +3,30 @@
33
module AnnotateRb
44
module ModelAnnotator
55
autoload :Annotator, 'annotate_rb/model_annotator/annotator'
6-
autoload :Helper, 'annotate_rb/model_annotator/helper'
7-
autoload :FilePatterns, 'annotate_rb/model_annotator/file_patterns'
86
autoload :Constants, 'annotate_rb/model_annotator/constants'
97
autoload :PatternGetter, 'annotate_rb/model_annotator/pattern_getter'
108
autoload :BadModelFileError, 'annotate_rb/model_annotator/bad_model_file_error'
119
autoload :FileNameResolver, 'annotate_rb/model_annotator/file_name_resolver'
12-
autoload :FileAnnotationRemover, 'annotate_rb/model_annotator/file_annotation_remover'
10+
autoload :SingleFileAnnotationRemover, 'annotate_rb/model_annotator/single_file_annotation_remover'
1311
autoload :AnnotationPatternGenerator, 'annotate_rb/model_annotator/annotation_pattern_generator'
1412
autoload :ModelClassGetter, 'annotate_rb/model_annotator/model_class_getter'
1513
autoload :ModelFilesGetter, 'annotate_rb/model_annotator/model_files_getter'
16-
autoload :FileAnnotator, 'annotate_rb/model_annotator/file_annotator'
17-
autoload :ModelFileAnnotator, 'annotate_rb/model_annotator/model_file_annotator'
14+
autoload :SingleFileAnnotator, 'annotate_rb/model_annotator/single_file_annotator'
1815
autoload :ModelWrapper, 'annotate_rb/model_annotator/model_wrapper'
19-
autoload :AnnotationGenerator, 'annotate_rb/model_annotator/annotation_generator'
20-
autoload :ColumnAttributesBuilder, 'annotate_rb/model_annotator/column_attributes_builder'
21-
autoload :ColumnTypeBuilder, 'annotate_rb/model_annotator/column_type_builder'
22-
autoload :ColumnWrapper, 'annotate_rb/model_annotator/column_wrapper'
23-
autoload :ColumnAnnotationBuilder, 'annotate_rb/model_annotator/column_annotation_builder'
24-
autoload :IndexAnnotationBuilder, 'annotate_rb/model_annotator/index_annotation_builder'
25-
autoload :ForeignKeyAnnotationBuilder, 'annotate_rb/model_annotator/foreign_key_annotation_builder'
16+
autoload :AnnotationBuilder, 'annotate_rb/model_annotator/annotation_builder'
17+
autoload :ColumnAnnotation, 'annotate_rb/model_annotator/column_annotation'
18+
autoload :IndexAnnotation, 'annotate_rb/model_annotator/index_annotation'
19+
autoload :ForeignKeyAnnotation, 'annotate_rb/model_annotator/foreign_key_annotation'
2620
autoload :RelatedFilesListBuilder, 'annotate_rb/model_annotator/related_files_list_builder'
2721
autoload :AnnotationDecider, 'annotate_rb/model_annotator/annotation_decider'
28-
autoload :FileAnnotatorInstruction, 'annotate_rb/model_annotator/file_annotator_instruction'
22+
autoload :SingleFileAnnotatorInstruction, 'annotate_rb/model_annotator/single_file_annotator_instruction'
23+
autoload :SingleFileRemoveAnnotationInstruction, 'annotate_rb/model_annotator/single_file_remove_annotation_instruction'
2924
autoload :AnnotationDiffGenerator, 'annotate_rb/model_annotator/annotation_diff_generator'
3025
autoload :AnnotationDiff, 'annotate_rb/model_annotator/annotation_diff'
26+
autoload :FileBuilder, 'annotate_rb/model_annotator/file_builder'
27+
autoload :MagicCommentParser, 'annotate_rb/model_annotator/magic_comment_parser'
28+
autoload :FileComponents, 'annotate_rb/model_annotator/file_components'
29+
autoload :ProjectAnnotator, 'annotate_rb/model_annotator/project_annotator'
30+
autoload :ProjectAnnotationRemover, 'annotate_rb/model_annotator/project_annotation_remover'
3131
end
3232
end

lib/annotate_rb/model_annotator/annotation_generator.rb renamed to lib/annotate_rb/model_annotator/annotation_builder.rb

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
module AnnotateRb
44
module ModelAnnotator
5-
class AnnotationGenerator
5+
class AnnotationBuilder
66
# Annotate Models plugin use this header
77
PREFIX = '== Schema Information'.freeze
88
PREFIX_MD = '## Schema Information'.freeze
@@ -18,7 +18,7 @@ def initialize(klass, options = {})
1818
@info = "" # TODO: Make array and build string that way
1919
end
2020

21-
def generate
21+
def build
2222
@info = "# #{header}\n"
2323
@info += schema_header_text
2424

@@ -33,15 +33,15 @@ def generate
3333
end
3434

3535
@info += @model.columns.map do |col|
36-
ColumnAnnotationBuilder.new(col, @model, max_size, @options).build
36+
ColumnAnnotation::AnnotationBuilder.new(col, @model, max_size, @options).build
3737
end.join
3838

3939
if @options[:show_indexes] && @model.table_exists?
40-
@info += IndexAnnotationBuilder.new(@model, @options).build
40+
@info += IndexAnnotation::AnnotationBuilder.new(@model, @options).build
4141
end
4242

4343
if @options[:show_foreign_keys] && @model.table_exists?
44-
@info += ForeignKeyAnnotationBuilder.new(@model, @options).build
44+
@info += ForeignKeyAnnotation::AnnotationBuilder.new(@model, @options).build
4545
end
4646

4747
@info += schema_footer_text

lib/annotate_rb/model_annotator/annotation_decider.rb

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -35,11 +35,11 @@ def annotate?
3535
return to_annotate
3636
rescue BadModelFileError => e
3737
unless @options[:ignore_unknown_models]
38-
$stderr.puts "Unable to annotate #{@file}: #{e.message}"
38+
$stderr.puts "Unable to process #{@file}: #{e.message}"
3939
$stderr.puts "\t" + e.backtrace.join("\n\t") if @options[:trace]
4040
end
4141
rescue StandardError => e
42-
$stderr.puts "Unable to annotate #{@file}: #{e.message}"
42+
$stderr.puts "Unable to process #{@file}: #{e.message}"
4343
$stderr.puts "\t" + e.backtrace.join("\n\t") if @options[:trace]
4444
end
4545

lib/annotate_rb/model_annotator/annotator.rb

Lines changed: 12 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -5,64 +5,24 @@ module ModelAnnotator
55
class Annotator
66
class << self
77
def do_annotations(options = {})
8-
annotated = []
9-
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
18-
end
19-
20-
if annotated.empty?
21-
puts 'Model files unchanged.'
22-
else
23-
puts "Annotated (#{annotated.length}): #{annotated.join(', ')}"
24-
end
8+
new(options).do_annotations
259
end
2610

2711
def remove_annotations(options = {})
28-
deannotated = []
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-
36-
begin
37-
klass = ModelClassGetter.call(file, options)
38-
if klass < ActiveRecord::Base && !klass.abstract_class?
39-
model_name = klass.name.underscore
40-
table_name = klass.table_name
41-
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
12+
new(options).remove_annotations
13+
end
14+
end
4715

48-
related_files.each do |f, _position_key|
49-
if File.exist?(f)
50-
FileAnnotationRemover.call(f, options)
51-
end
52-
end
53-
end
16+
def initialize(options)
17+
@options = options
18+
end
5419

55-
if deannotated_klass
56-
deannotated << klass
57-
end
58-
rescue StandardError => e
59-
$stderr.puts "Unable to deannotate #{File.join(file)}: #{e.message}"
60-
$stderr.puts "\t" + e.backtrace.join("\n\t") if options[:trace]
61-
end
62-
end
20+
def do_annotations
21+
ProjectAnnotator.new(@options).annotate
22+
end
6323

64-
puts "Removed annotations from: #{deannotated.join(', ')}"
65-
end
24+
def remove_annotations
25+
ProjectAnnotationRemover.new(@options).remove_annotations
6626
end
6727
end
6828
end
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
# frozen_string_literal: true
2+
3+
module AnnotateRb
4+
module ModelAnnotator
5+
module ColumnAnnotation
6+
autoload :AttributesBuilder, 'annotate_rb/model_annotator/column_annotation/attributes_builder'
7+
autoload :TypeBuilder, 'annotate_rb/model_annotator/column_annotation/type_builder'
8+
autoload :ColumnWrapper, 'annotate_rb/model_annotator/column_annotation/column_wrapper'
9+
autoload :AnnotationBuilder, 'annotate_rb/model_annotator/column_annotation/annotation_builder'
10+
end
11+
end
12+
end
Lines changed: 121 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,121 @@
1+
# frozen_string_literal: true
2+
3+
module AnnotateRb
4+
module ModelAnnotator
5+
module ColumnAnnotation
6+
class AnnotationBuilder
7+
BARE_TYPE_ALLOWANCE = 16
8+
MD_TYPE_ALLOWANCE = 18
9+
10+
def initialize(column, model, max_size, options)
11+
@column = column
12+
@model = model
13+
@max_size = max_size
14+
@options = options
15+
end
16+
17+
def build
18+
result = ''
19+
20+
is_primary_key = is_column_primary_key?(@model, @column.name)
21+
22+
table_indices = @model.retrieve_indexes_from_table
23+
column_indices = table_indices.select { |ind| ind.columns.include?(@column.name) }
24+
25+
column_attributes = AttributesBuilder.new(@column, @options, is_primary_key, column_indices).build
26+
formatted_column_type = TypeBuilder.new(@column, @options).build
27+
28+
col_name = if @model.with_comments? && @column.comment
29+
"#{@column.name}(#{@column.comment.gsub(/\n/, '\\n')})"
30+
else
31+
@column.name
32+
end
33+
34+
if @options[:format_rdoc]
35+
result += format("# %-#{@max_size}.#{@max_size}s<tt>%s</tt>",
36+
"*#{col_name}*::",
37+
column_attributes.unshift(formatted_column_type).join(', ')).rstrip + "\n"
38+
elsif @options[:format_yard]
39+
result += sprintf("# @!attribute #{col_name}") + "\n"
40+
41+
if @column.respond_to?(:array) && @column.array
42+
ruby_class = "Array<#{map_col_type_to_ruby_classes(formatted_column_type)}>"
43+
else
44+
ruby_class = map_col_type_to_ruby_classes(formatted_column_type)
45+
end
46+
47+
result += sprintf("# @return [#{ruby_class}]") + "\n"
48+
elsif @options[:format_markdown]
49+
name_remainder = @max_size - col_name.length - non_ascii_length(col_name)
50+
type_remainder = (MD_TYPE_ALLOWANCE - 2) - formatted_column_type.length
51+
result += format("# **`%s`**%#{name_remainder}s | `%s`%#{type_remainder}s | `%s`",
52+
col_name,
53+
' ',
54+
formatted_column_type,
55+
' ',
56+
column_attributes.join(', ').rstrip).gsub('``', ' ').rstrip + "\n"
57+
else
58+
result += format_default(col_name, @max_size, formatted_column_type, column_attributes)
59+
end
60+
61+
result
62+
end
63+
64+
private
65+
66+
def non_ascii_length(string)
67+
string.to_s.chars.reject(&:ascii_only?).length
68+
end
69+
70+
def mb_chars_ljust(string, length)
71+
string = string.to_s
72+
padding = length - Helper.width(string)
73+
if padding.positive?
74+
string + (' ' * padding)
75+
else
76+
string[0..(length - 1)]
77+
end
78+
end
79+
80+
def map_col_type_to_ruby_classes(col_type)
81+
case col_type
82+
when 'integer' then Integer.to_s
83+
when 'float' then Float.to_s
84+
when 'decimal' then BigDecimal.to_s
85+
when 'datetime', 'timestamp', 'time' then Time.to_s
86+
when 'date' then Date.to_s
87+
when 'text', 'string', 'binary', 'inet', 'uuid' then String.to_s
88+
when 'json', 'jsonb' then Hash.to_s
89+
when 'boolean' then 'Boolean'
90+
end
91+
end
92+
93+
def format_default(col_name, max_size, col_type, attrs)
94+
format('# %s:%s %s',
95+
mb_chars_ljust(col_name, max_size),
96+
mb_chars_ljust(col_type, BARE_TYPE_ALLOWANCE),
97+
attrs.join(', ')).rstrip + "\n"
98+
end
99+
100+
# TODO: Simplify this conditional
101+
def is_column_primary_key?(model, column_name)
102+
if model.primary_key
103+
if model.primary_key.is_a?(Array)
104+
# If the model has multiple primary keys, check if this column is one of them
105+
if model.primary_key.collect(&:to_sym).include?(column_name.to_sym)
106+
return true
107+
end
108+
else
109+
# If model has 1 primary key, check if this column is it
110+
if column_name.to_sym == model.primary_key.to_sym
111+
return true
112+
end
113+
end
114+
end
115+
116+
false
117+
end
118+
end
119+
end
120+
end
121+
end

0 commit comments

Comments
 (0)