Skip to content

Commit

Permalink
Refactor ModelAnnotator again (#28)
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
drwl authored May 23, 2023
1 parent 1ce7d6d commit 2472836
Show file tree
Hide file tree
Showing 53 changed files with 1,571 additions and 1,212 deletions.
1 change: 1 addition & 0 deletions lib/annotate_rb.rb
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@

require 'annotate_rb/active_record_patch'

require_relative 'annotate_rb/helper'
require_relative 'annotate_rb/core'
require_relative 'annotate_rb/commands'
require_relative 'annotate_rb/parser'
Expand Down
16 changes: 16 additions & 0 deletions lib/annotate_rb/helper.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
# frozen_string_literal: true

module AnnotateRb
module Helper
class << self
def width(string)
string.chars.inject(0) { |acc, elem| acc + (elem.bytesize == 3 ? 2 : 1) }
end

# TODO: Find another implementation that doesn't depend on ActiveSupport
def fallback(*args)
args.compact.detect(&:present?)
end
end
end
end
26 changes: 13 additions & 13 deletions lib/annotate_rb/model_annotator.rb
Original file line number Diff line number Diff line change
Expand Up @@ -3,30 +3,30 @@
module AnnotateRb
module ModelAnnotator
autoload :Annotator, 'annotate_rb/model_annotator/annotator'
autoload :Helper, 'annotate_rb/model_annotator/helper'
autoload :FilePatterns, 'annotate_rb/model_annotator/file_patterns'
autoload :Constants, 'annotate_rb/model_annotator/constants'
autoload :PatternGetter, 'annotate_rb/model_annotator/pattern_getter'
autoload :BadModelFileError, 'annotate_rb/model_annotator/bad_model_file_error'
autoload :FileNameResolver, 'annotate_rb/model_annotator/file_name_resolver'
autoload :FileAnnotationRemover, 'annotate_rb/model_annotator/file_annotation_remover'
autoload :SingleFileAnnotationRemover, 'annotate_rb/model_annotator/single_file_annotation_remover'
autoload :AnnotationPatternGenerator, 'annotate_rb/model_annotator/annotation_pattern_generator'
autoload :ModelClassGetter, 'annotate_rb/model_annotator/model_class_getter'
autoload :ModelFilesGetter, 'annotate_rb/model_annotator/model_files_getter'
autoload :FileAnnotator, 'annotate_rb/model_annotator/file_annotator'
autoload :ModelFileAnnotator, 'annotate_rb/model_annotator/model_file_annotator'
autoload :SingleFileAnnotator, 'annotate_rb/model_annotator/single_file_annotator'
autoload :ModelWrapper, 'annotate_rb/model_annotator/model_wrapper'
autoload :AnnotationGenerator, 'annotate_rb/model_annotator/annotation_generator'
autoload :ColumnAttributesBuilder, 'annotate_rb/model_annotator/column_attributes_builder'
autoload :ColumnTypeBuilder, 'annotate_rb/model_annotator/column_type_builder'
autoload :ColumnWrapper, 'annotate_rb/model_annotator/column_wrapper'
autoload :ColumnAnnotationBuilder, 'annotate_rb/model_annotator/column_annotation_builder'
autoload :IndexAnnotationBuilder, 'annotate_rb/model_annotator/index_annotation_builder'
autoload :ForeignKeyAnnotationBuilder, 'annotate_rb/model_annotator/foreign_key_annotation_builder'
autoload :AnnotationBuilder, 'annotate_rb/model_annotator/annotation_builder'
autoload :ColumnAnnotation, 'annotate_rb/model_annotator/column_annotation'
autoload :IndexAnnotation, 'annotate_rb/model_annotator/index_annotation'
autoload :ForeignKeyAnnotation, 'annotate_rb/model_annotator/foreign_key_annotation'
autoload :RelatedFilesListBuilder, 'annotate_rb/model_annotator/related_files_list_builder'
autoload :AnnotationDecider, 'annotate_rb/model_annotator/annotation_decider'
autoload :FileAnnotatorInstruction, 'annotate_rb/model_annotator/file_annotator_instruction'
autoload :SingleFileAnnotatorInstruction, 'annotate_rb/model_annotator/single_file_annotator_instruction'
autoload :SingleFileRemoveAnnotationInstruction, 'annotate_rb/model_annotator/single_file_remove_annotation_instruction'
autoload :AnnotationDiffGenerator, 'annotate_rb/model_annotator/annotation_diff_generator'
autoload :AnnotationDiff, 'annotate_rb/model_annotator/annotation_diff'
autoload :FileBuilder, 'annotate_rb/model_annotator/file_builder'
autoload :MagicCommentParser, 'annotate_rb/model_annotator/magic_comment_parser'
autoload :FileComponents, 'annotate_rb/model_annotator/file_components'
autoload :ProjectAnnotator, 'annotate_rb/model_annotator/project_annotator'
autoload :ProjectAnnotationRemover, 'annotate_rb/model_annotator/project_annotation_remover'
end
end
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

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

def generate
def build
@info = "# #{header}\n"
@info += schema_header_text

Expand All @@ -33,15 +33,15 @@ def generate
end

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

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

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

@info += schema_footer_text
Expand Down
4 changes: 2 additions & 2 deletions lib/annotate_rb/model_annotator/annotation_decider.rb
Original file line number Diff line number Diff line change
Expand Up @@ -35,11 +35,11 @@ def annotate?
return to_annotate
rescue BadModelFileError => e
unless @options[:ignore_unknown_models]
$stderr.puts "Unable to annotate #{@file}: #{e.message}"
$stderr.puts "Unable to process #{@file}: #{e.message}"
$stderr.puts "\t" + e.backtrace.join("\n\t") if @options[:trace]
end
rescue StandardError => e
$stderr.puts "Unable to annotate #{@file}: #{e.message}"
$stderr.puts "Unable to process #{@file}: #{e.message}"
$stderr.puts "\t" + e.backtrace.join("\n\t") if @options[:trace]
end

Expand Down
64 changes: 12 additions & 52 deletions lib/annotate_rb/model_annotator/annotator.rb
Original file line number Diff line number Diff line change
Expand Up @@ -5,64 +5,24 @@ module ModelAnnotator
class Annotator
class << self
def do_annotations(options = {})
annotated = []

model_files_to_consider = ModelFilesGetter.call(options)

model_files_to_consider.each do |path, filename|
file = File.join(path, filename)

if AnnotationDecider.new(file, options).annotate?
ModelFileAnnotator.call(annotated, file, options)
end
end

if annotated.empty?
puts 'Model files unchanged.'
else
puts "Annotated (#{annotated.length}): #{annotated.join(', ')}"
end
new(options).do_annotations
end

def remove_annotations(options = {})
deannotated = []

model_files_to_consider = ModelFilesGetter.call(options)

model_files_to_consider.each do |path, filename|
deannotated_klass = false
file = File.join(path, filename)

begin
klass = ModelClassGetter.call(file, options)
if klass < ActiveRecord::Base && !klass.abstract_class?
model_name = klass.name.underscore
table_name = klass.table_name

if FileAnnotationRemover.call(file, options)
deannotated_klass = true
end

related_files = RelatedFilesListBuilder.new(file, model_name, table_name, options).build
new(options).remove_annotations
end
end

related_files.each do |f, _position_key|
if File.exist?(f)
FileAnnotationRemover.call(f, options)
end
end
end
def initialize(options)
@options = options
end

if deannotated_klass
deannotated << klass
end
rescue StandardError => e
$stderr.puts "Unable to deannotate #{File.join(file)}: #{e.message}"
$stderr.puts "\t" + e.backtrace.join("\n\t") if options[:trace]
end
end
def do_annotations
ProjectAnnotator.new(@options).annotate
end

puts "Removed annotations from: #{deannotated.join(', ')}"
end
def remove_annotations
ProjectAnnotationRemover.new(@options).remove_annotations
end
end
end
Expand Down
12 changes: 12 additions & 0 deletions lib/annotate_rb/model_annotator/column_annotation.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
# frozen_string_literal: true

module AnnotateRb
module ModelAnnotator
module ColumnAnnotation
autoload :AttributesBuilder, 'annotate_rb/model_annotator/column_annotation/attributes_builder'
autoload :TypeBuilder, 'annotate_rb/model_annotator/column_annotation/type_builder'
autoload :ColumnWrapper, 'annotate_rb/model_annotator/column_annotation/column_wrapper'
autoload :AnnotationBuilder, 'annotate_rb/model_annotator/column_annotation/annotation_builder'
end
end
end
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
# frozen_string_literal: true

module AnnotateRb
module ModelAnnotator
module ColumnAnnotation
class AnnotationBuilder
BARE_TYPE_ALLOWANCE = 16
MD_TYPE_ALLOWANCE = 18

def initialize(column, model, max_size, options)
@column = column
@model = model
@max_size = max_size
@options = options
end

def build
result = ''

is_primary_key = is_column_primary_key?(@model, @column.name)

table_indices = @model.retrieve_indexes_from_table
column_indices = table_indices.select { |ind| ind.columns.include?(@column.name) }

column_attributes = AttributesBuilder.new(@column, @options, is_primary_key, column_indices).build
formatted_column_type = TypeBuilder.new(@column, @options).build

col_name = if @model.with_comments? && @column.comment
"#{@column.name}(#{@column.comment.gsub(/\n/, '\\n')})"
else
@column.name
end

if @options[:format_rdoc]
result += format("# %-#{@max_size}.#{@max_size}s<tt>%s</tt>",
"*#{col_name}*::",
column_attributes.unshift(formatted_column_type).join(', ')).rstrip + "\n"
elsif @options[:format_yard]
result += sprintf("# @!attribute #{col_name}") + "\n"

if @column.respond_to?(:array) && @column.array
ruby_class = "Array<#{map_col_type_to_ruby_classes(formatted_column_type)}>"
else
ruby_class = map_col_type_to_ruby_classes(formatted_column_type)
end

result += sprintf("# @return [#{ruby_class}]") + "\n"
elsif @options[:format_markdown]
name_remainder = @max_size - col_name.length - non_ascii_length(col_name)
type_remainder = (MD_TYPE_ALLOWANCE - 2) - formatted_column_type.length
result += format("# **`%s`**%#{name_remainder}s | `%s`%#{type_remainder}s | `%s`",
col_name,
' ',
formatted_column_type,
' ',
column_attributes.join(', ').rstrip).gsub('``', ' ').rstrip + "\n"
else
result += format_default(col_name, @max_size, formatted_column_type, column_attributes)
end

result
end

private

def non_ascii_length(string)
string.to_s.chars.reject(&:ascii_only?).length
end

def mb_chars_ljust(string, length)
string = string.to_s
padding = length - Helper.width(string)
if padding.positive?
string + (' ' * padding)
else
string[0..(length - 1)]
end
end

def map_col_type_to_ruby_classes(col_type)
case col_type
when 'integer' then Integer.to_s
when 'float' then Float.to_s
when 'decimal' then BigDecimal.to_s
when 'datetime', 'timestamp', 'time' then Time.to_s
when 'date' then Date.to_s
when 'text', 'string', 'binary', 'inet', 'uuid' then String.to_s
when 'json', 'jsonb' then Hash.to_s
when 'boolean' then 'Boolean'
end
end

def format_default(col_name, max_size, col_type, attrs)
format('# %s:%s %s',
mb_chars_ljust(col_name, max_size),
mb_chars_ljust(col_type, BARE_TYPE_ALLOWANCE),
attrs.join(', ')).rstrip + "\n"
end

# TODO: Simplify this conditional
def is_column_primary_key?(model, column_name)
if model.primary_key
if model.primary_key.is_a?(Array)
# If the model has multiple primary keys, check if this column is one of them
if model.primary_key.collect(&:to_sym).include?(column_name.to_sym)
return true
end
else
# If model has 1 primary key, check if this column is it
if column_name.to_sym == model.primary_key.to_sym
return true
end
end
end

false
end
end
end
end
end
Loading

0 comments on commit 2472836

Please sign in to comment.