annotaterb 4.0.0.beta.1 → 4.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (32) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +91 -1
  3. data/VERSION +1 -1
  4. data/lib/annotate_rb/model_annotator/annotation_decider.rb +62 -0
  5. data/lib/annotate_rb/model_annotator/annotation_generator.rb +94 -0
  6. data/lib/annotate_rb/model_annotator/annotator.rb +27 -31
  7. data/lib/annotate_rb/model_annotator/column_annotation_builder.rb +92 -0
  8. data/lib/annotate_rb/model_annotator/column_attributes_builder.rb +102 -0
  9. data/lib/annotate_rb/model_annotator/column_type_builder.rb +51 -0
  10. data/lib/annotate_rb/model_annotator/column_wrapper.rb +84 -0
  11. data/lib/annotate_rb/model_annotator/constants.rb +2 -2
  12. data/lib/annotate_rb/model_annotator/file_annotator.rb +6 -2
  13. data/lib/annotate_rb/model_annotator/file_annotator_instruction.rb +17 -0
  14. data/lib/annotate_rb/model_annotator/foreign_key_annotation_builder.rb +55 -0
  15. data/lib/annotate_rb/model_annotator/helper.rb +75 -22
  16. data/lib/annotate_rb/model_annotator/index_annotation_builder.rb +74 -0
  17. data/lib/annotate_rb/model_annotator/model_file_annotator.rb +21 -84
  18. data/lib/annotate_rb/model_annotator/model_files_getter.rb +4 -2
  19. data/lib/annotate_rb/model_annotator/model_wrapper.rb +155 -0
  20. data/lib/annotate_rb/model_annotator/related_files_list_builder.rb +137 -0
  21. data/lib/annotate_rb/model_annotator.rb +11 -1
  22. data/lib/annotate_rb/options.rb +16 -9
  23. data/lib/annotate_rb/parser.rb +1 -15
  24. data/lib/annotate_rb.rb +0 -1
  25. data/lib/generators/annotate_rb/install/USAGE +7 -0
  26. data/lib/generators/annotate_rb/install/install_generator.rb +14 -0
  27. metadata +18 -9
  28. data/lib/annotate_rb/env.rb +0 -30
  29. data/lib/annotate_rb/model_annotator/schema_info.rb +0 -480
  30. data/lib/generators/annotate_rb/USAGE +0 -4
  31. data/lib/generators/annotate_rb/install_generator.rb +0 -15
  32. /data/lib/generators/annotate_rb/{templates/auto_annotate_models.rake → install/templates/annotate_rb.rake} +0 -0
@@ -0,0 +1,84 @@
1
+ # frozen_string_literal: true
2
+
3
+ module AnnotateRb
4
+ module ModelAnnotator
5
+ class ColumnWrapper
6
+ def initialize(column)
7
+ @column = column
8
+ end
9
+
10
+ def default
11
+ # Note: Used to be klass.column_defaults[name], where name is the column name.
12
+ # Looks to be identical, but keeping note here in case there are differences.
13
+ _column_default = @column.default
14
+ end
15
+
16
+ def default_string
17
+ Helper.quote(@column.default)
18
+ end
19
+
20
+ def type
21
+ @column.type
22
+ end
23
+
24
+ def column_type_string
25
+ if (@column.respond_to?(:bigint?) && @column.bigint?) || /\Abigint\b/ =~ @column.sql_type
26
+ 'bigint'
27
+ else
28
+ (@column.type || @column.sql_type).to_s
29
+ end
30
+ end
31
+
32
+ def unsigned?
33
+ @column.respond_to?(:unsigned?) && @column.unsigned?
34
+ end
35
+
36
+ def null
37
+ @column.null
38
+ end
39
+
40
+ def precision
41
+ @column.precision
42
+ end
43
+
44
+ def scale
45
+ @column.scale
46
+ end
47
+
48
+ def limit
49
+ @column.limit
50
+ end
51
+
52
+ def geometry_type?
53
+ @column.respond_to?(:geometry_type)
54
+ end
55
+
56
+ def geometry_type
57
+ # TODO: Check if we need to check if it responds before accessing the geometry type
58
+ @column.geometry_type
59
+ end
60
+
61
+ def geometric_type?
62
+ @column.respond_to?(:geometric_type)
63
+ end
64
+
65
+ def geometric_type
66
+ # TODO: Check if we need to check if it responds before accessing the geometric type
67
+ @column.geometric_type
68
+ end
69
+
70
+ def srid
71
+ # TODO: Check if we need to check if it responds before accessing the srid
72
+ @column.srid
73
+ end
74
+
75
+ def array?
76
+ @column.respond_to?(:array) && @column.array
77
+ end
78
+
79
+ def name
80
+ @column.name
81
+ end
82
+ end
83
+ end
84
+ end
@@ -1,8 +1,6 @@
1
1
  module AnnotateRb
2
2
  module ModelAnnotator
3
3
  module Constants
4
- TRUE_RE = /^(true|t|yes|y|1)$/i.freeze
5
-
6
4
  ##
7
5
  # The set of available options to customize the behavior of Annotate.
8
6
  #
@@ -17,6 +15,8 @@ module AnnotateRb
17
15
  ALL_ANNOTATE_OPTIONS = ::AnnotateRb::Options::ALL_OPTION_KEYS
18
16
 
19
17
  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
20
20
  end
21
21
  end
22
22
  end
@@ -4,6 +4,10 @@ module AnnotateRb
4
4
  module ModelAnnotator
5
5
  class FileAnnotator
6
6
  class << self
7
+ def call_with_instructions(instruction)
8
+ call(instruction.file, instruction.annotation, instruction.position, instruction.options)
9
+ end
10
+
7
11
  # Add a schema block to a file. If the file already contains
8
12
  # a schema info block (a comment starting with "== Schema Information"),
9
13
  # check if it matches the block that is already there. If so, leave it be.
@@ -33,7 +37,7 @@ module AnnotateRb
33
37
 
34
38
  return false if old_columns == new_columns && !options[:force]
35
39
 
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]
37
41
 
38
42
  # Replace inline the old schema info with the new schema info
39
43
  wrapper_open = options[:wrapper_open] ? "# #{options[:wrapper_open]}\n" : ""
@@ -47,7 +51,7 @@ module AnnotateRb
47
51
  # need to insert it in correct position
48
52
  if old_annotation.empty? || options[:force]
49
53
  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, '')
51
55
 
52
56
  annotation_pattern = AnnotationPatternGenerator.call(options)
53
57
  old_content.sub!(annotation_pattern, '')
@@ -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
@@ -0,0 +1,55 @@
1
+ # frozen_string_literal: true
2
+
3
+ module AnnotateRb
4
+ module ModelAnnotator
5
+ class ForeignKeyAnnotationBuilder
6
+ def initialize(model, options)
7
+ @model = model
8
+ @options = options
9
+ end
10
+
11
+ def build
12
+ fk_info = if @options[:format_markdown]
13
+ "#\n# ### Foreign Keys\n#\n"
14
+ else
15
+ "#\n# Foreign Keys\n#\n"
16
+ end
17
+
18
+ return '' unless @model.connection.respond_to?(:supports_foreign_keys?) &&
19
+ @model.connection.supports_foreign_keys? && @model.connection.respond_to?(:foreign_keys)
20
+
21
+ foreign_keys = @model.connection.foreign_keys(@model.table_name)
22
+ return '' if foreign_keys.empty?
23
+
24
+ format_name = lambda do |fk|
25
+ return fk.options[:column] if fk.name.blank?
26
+
27
+ @options[:show_complete_foreign_keys] ? fk.name : fk.name.gsub(/(?<=^fk_rails_)[0-9a-f]{10}$/, '...')
28
+ end
29
+
30
+ max_size = foreign_keys.map(&format_name).map(&:size).max + 1
31
+ foreign_keys.sort_by { |fk| [format_name.call(fk), fk.column] }.each do |fk|
32
+ ref_info = "#{fk.column} => #{fk.to_table}.#{fk.primary_key}"
33
+ constraints_info = ''
34
+ constraints_info += "ON DELETE => #{fk.on_delete} " if fk.on_delete
35
+ constraints_info += "ON UPDATE => #{fk.on_update} " if fk.on_update
36
+ constraints_info = constraints_info.strip
37
+
38
+ fk_info += if @options[:format_markdown]
39
+ format("# * `%s`%s:\n# * **`%s`**\n",
40
+ format_name.call(fk),
41
+ constraints_info.blank? ? '' : " (_#{constraints_info}_)",
42
+ ref_info)
43
+ else
44
+ format("# %-#{max_size}.#{max_size}s %s %s",
45
+ format_name.call(fk),
46
+ "(#{ref_info})",
47
+ constraints_info).rstrip + "\n"
48
+ end
49
+ end
50
+
51
+ fk_info
52
+ end
53
+ end
54
+ end
55
+ end
@@ -3,51 +3,104 @@
3
3
  module AnnotateRb
4
4
  module ModelAnnotator
5
5
  module Helper
6
- MATCHED_TYPES = %w(test fixture factory serializer scaffold controller helper).freeze
6
+ INDEX_CLAUSES = {
7
+ unique: {
8
+ default: 'UNIQUE',
9
+ markdown: '_unique_'
10
+ },
11
+ where: {
12
+ default: 'WHERE',
13
+ markdown: '_where_'
14
+ },
15
+ using: {
16
+ default: 'USING',
17
+ markdown: '_using_'
18
+ }
19
+ }.freeze
7
20
 
8
21
  class << self
9
- def matched_types(options)
10
- types = MATCHED_TYPES.dup
11
- types << 'admin' if options[:active_admin] =~ Constants::TRUE_RE && !types.include?('admin')
12
- types << 'additional_file_patterns' if options[:additional_file_patterns].present?
22
+ def mb_chars_ljust(string, length)
23
+ string = string.to_s
24
+ padding = length - Helper.width(string)
25
+ if padding.positive?
26
+ string + (' ' * padding)
27
+ else
28
+ string[0..(length - 1)]
29
+ end
30
+ end
13
31
 
14
- types
32
+ def index_unique_info(index, format = :default)
33
+ index.unique ? " #{INDEX_CLAUSES[:unique][format]}" : ''
15
34
  end
16
35
 
17
- def magic_comments_as_string(content)
18
- magic_comments = content.scan(Annotator::MAGIC_COMMENT_MATCHER).flatten.compact
36
+ def index_where_info(index, format = :default)
37
+ value = index.try(:where).try(:to_s)
38
+ if value.blank?
39
+ ''
40
+ else
41
+ " #{INDEX_CLAUSES[:where][format]} #{value}"
42
+ end
43
+ end
19
44
 
20
- if magic_comments.any?
21
- magic_comments.join
45
+ def index_using_info(index, format = :default)
46
+ value = index.try(:using) && index.using.try(:to_sym)
47
+ if !value.blank? && value != :btree
48
+ " #{INDEX_CLAUSES[:using][format]} #{value}"
22
49
  else
23
50
  ''
24
51
  end
25
52
  end
26
53
 
27
- def skip_on_migration?
28
- Env.read('ANNOTATE_SKIP_ON_DB_MIGRATE') =~ Constants::TRUE_RE || Env.read('skip_on_db_migrate') =~ Constants::TRUE_RE
54
+ def map_col_type_to_ruby_classes(col_type)
55
+ case col_type
56
+ when 'integer' then Integer.to_s
57
+ when 'float' then Float.to_s
58
+ when 'decimal' then BigDecimal.to_s
59
+ when 'datetime', 'timestamp', 'time' then Time.to_s
60
+ when 'date' then Date.to_s
61
+ when 'text', 'string', 'binary', 'inet', 'uuid' then String.to_s
62
+ when 'json', 'jsonb' then Hash.to_s
63
+ when 'boolean' then 'Boolean'
64
+ end
65
+ end
66
+
67
+ def non_ascii_length(string)
68
+ string.to_s.chars.reject(&:ascii_only?).length
29
69
  end
30
70
 
31
- def include_routes?
32
- Env.read('routes') =~ Constants::TRUE_RE
71
+ # Simple quoting for the default column value
72
+ def quote(value)
73
+ case value
74
+ when NilClass then 'NULL'
75
+ when TrueClass then 'TRUE'
76
+ when FalseClass then 'FALSE'
77
+ when Float, Integer then value.to_s
78
+ # BigDecimals need to be output in a non-normalized form and quoted.
79
+ when BigDecimal then value.to_s('F')
80
+ when Array then value.map { |v| quote(v) }
81
+ else
82
+ value.inspect
83
+ end
33
84
  end
34
85
 
35
- def include_models?
36
- Env.read('models') =~ Constants::TRUE_RE
86
+ def width(string)
87
+ string.chars.inject(0) { |acc, elem| acc + (elem.bytesize == 3 ? 2 : 1) }
37
88
  end
38
89
 
39
- def true?(val)
40
- val.present? && Constants::TRUE_RE.match?(val)
90
+ def magic_comments_as_string(content)
91
+ magic_comments = content.scan(Constants::MAGIC_COMMENT_MATCHER).flatten.compact
92
+
93
+ if magic_comments.any?
94
+ magic_comments.join
95
+ else
96
+ ''
97
+ end
41
98
  end
42
99
 
43
100
  # TODO: Find another implementation that doesn't depend on ActiveSupport
44
101
  def fallback(*args)
45
102
  args.compact.detect(&:present?)
46
103
  end
47
-
48
- def reset_options(options)
49
- options.flatten.each { |key| Env.write(key, nil) }
50
- end
51
104
  end
52
105
  end
53
106
  end
@@ -0,0 +1,74 @@
1
+ # frozen_string_literal: true
2
+
3
+ module AnnotateRb
4
+ module ModelAnnotator
5
+ class IndexAnnotationBuilder
6
+ def initialize(model, options)
7
+ @model = model
8
+ @options = options
9
+ end
10
+
11
+ def build
12
+ index_info = if @options[:format_markdown]
13
+ "#\n# ### Indexes\n#\n"
14
+ else
15
+ "#\n# Indexes\n#\n"
16
+ end
17
+
18
+ indexes = @model.retrieve_indexes_from_table
19
+ return '' if indexes.empty?
20
+
21
+ max_size = indexes.collect { |index| index.name.size }.max + 1
22
+ indexes.sort_by(&:name).each do |index|
23
+ index_info += if @options[:format_markdown]
24
+ final_index_string_in_markdown(index)
25
+ else
26
+ final_index_string(index, max_size)
27
+ end
28
+ end
29
+
30
+ index_info
31
+ end
32
+
33
+ private
34
+
35
+ def final_index_string_in_markdown(index)
36
+ details = format(
37
+ '%s%s%s',
38
+ Helper.index_unique_info(index, :markdown),
39
+ Helper.index_where_info(index, :markdown),
40
+ Helper.index_using_info(index, :markdown)
41
+ ).strip
42
+ details = " (#{details})" unless details.blank?
43
+
44
+ format(
45
+ "# * `%s`%s:\n# * **`%s`**\n",
46
+ index.name,
47
+ details,
48
+ index_columns_info(index).join("`**\n# * **`")
49
+ )
50
+ end
51
+
52
+ def final_index_string(index, max_size)
53
+ format(
54
+ "# %-#{max_size}.#{max_size}s %s%s%s%s",
55
+ index.name,
56
+ "(#{index_columns_info(index).join(',')})",
57
+ Helper.index_unique_info(index),
58
+ Helper.index_where_info(index),
59
+ Helper.index_using_info(index)
60
+ ).rstrip + "\n"
61
+ end
62
+
63
+ def index_columns_info(index)
64
+ Array(index.columns).map do |col|
65
+ if index.try(:orders) && index.orders[col.to_s]
66
+ "#{col} #{index.orders[col.to_s].upcase}"
67
+ else
68
+ col.to_s.gsub("\r", '\r').gsub("\n", '\n')
69
+ end
70
+ end
71
+ end
72
+ end
73
+ end
74
+ end
@@ -2,36 +2,21 @@
2
2
 
3
3
  module AnnotateRb
4
4
  module ModelAnnotator
5
- # Not sure yet what the difference is between this and FileAnnotator
5
+ # Annotates a model file and its related files (controllers, factories, etc)
6
6
  class ModelFileAnnotator
7
7
  class << self
8
- def call(annotated, file, header, options)
8
+ def call(annotated, file, options)
9
9
  begin
10
- return false if /#{Constants::SKIP_ANNOTATION_PREFIX}.*/ =~ (File.exist?(file) ? File.read(file) : '')
11
10
  klass = ModelClassGetter.call(file, options)
12
11
 
13
- klass_is_a_class = klass.is_a?(Class)
14
- klass_inherits_active_record_base = klass < ActiveRecord::Base
15
- klass_is_not_abstract = !klass.abstract_class?
16
- klass_table_exists = klass.table_exists?
17
-
18
- not_sure_this_conditional = (!options[:exclude_sti_subclasses] || !(klass.superclass < ActiveRecord::Base && klass.table_name == klass.superclass.table_name))
19
-
20
- annotate_conditions = [
21
- klass_is_a_class,
22
- klass_inherits_active_record_base,
23
- not_sure_this_conditional,
24
- klass_is_not_abstract,
25
- klass_table_exists
26
- ]
27
-
28
- do_annotate = annotate_conditions.all?
29
-
30
- if do_annotate
31
- files_annotated = annotate(klass, file, header, options)
32
- annotated.concat(files_annotated)
12
+ instructions = build_instructions(klass, file, options)
13
+ instructions.each do |instruction|
14
+ if FileAnnotator.call_with_instructions(instruction)
15
+ annotated << instruction.file
16
+ end
33
17
  end
34
18
 
19
+ annotated
35
20
  rescue BadModelFileError => e
36
21
  unless options[:ignore_unknown_models]
37
22
  $stderr.puts "Unable to annotate #{file}: #{e.message}"
@@ -45,72 +30,24 @@ module AnnotateRb
45
30
 
46
31
  private
47
32
 
48
- # Given the name of an ActiveRecord class, create a schema
49
- # info block (basically a comment containing information
50
- # on the columns and their types) and put it at the front
51
- # of the model and fixture source files.
52
- #
53
- # === Options (opts)
54
- # :position_in_class<Symbol>:: where to place the annotated section in model file
55
- # :position_in_test<Symbol>:: where to place the annotated section in test/spec file(s)
56
- # :position_in_fixture<Symbol>:: where to place the annotated section in fixture file
57
- # :position_in_factory<Symbol>:: where to place the annotated section in factory file
58
- # :position_in_serializer<Symbol>:: where to place the annotated section in serializer file
59
- # :exclude_tests<Symbol>:: whether to skip modification of test/spec files
60
- # :exclude_fixtures<Symbol>:: whether to skip modification of fixture files
61
- # :exclude_factories<Symbol>:: whether to skip modification of factory files
62
- # :exclude_serializers<Symbol>:: whether to skip modification of serializer files
63
- # :exclude_scaffolds<Symbol>:: whether to skip modification of scaffold files
64
- # :exclude_controllers<Symbol>:: whether to skip modification of controller files
65
- # :exclude_helpers<Symbol>:: whether to skip modification of helper files
66
- # :exclude_sti_subclasses<Symbol>:: whether to skip modification of files for STI subclasses
67
- #
68
- # == Returns:
69
- # an array of file names that were annotated.
70
- #
71
- def annotate(klass, file, header, options = {})
72
- begin
73
- klass.reset_column_information
74
- info = SchemaInfo.generate(klass, header, options)
75
- model_name = klass.name.underscore
76
- table_name = klass.table_name
77
- model_file_name = File.join(file)
78
- annotated = []
79
-
80
- if FileAnnotator.call(model_file_name, info, :position_in_class, options)
81
- annotated << model_file_name
82
- end
83
-
84
- Helper.matched_types(options).each do |key|
85
- exclusion_key = "exclude_#{key.pluralize}".to_sym
86
- position_key = "position_in_#{key}".to_sym
87
-
88
- # Same options for active_admin models
89
- if key == 'admin'
90
- exclusion_key = 'exclude_class'.to_sym
91
- position_key = 'position_in_class'.to_sym
92
- end
33
+ def build_instructions(klass, file, options = {})
34
+ instructions = []
93
35
 
94
- next if options[exclusion_key]
36
+ klass.reset_column_information
37
+ annotation = AnnotationGenerator.new(klass, options).generate
38
+ model_name = klass.name.underscore
39
+ table_name = klass.table_name
95
40
 
96
- patterns = PatternGetter.call(options, key)
41
+ model_instruction = FileAnnotatorInstruction.new(file, annotation, :position_in_class, options)
42
+ instructions << model_instruction
97
43
 
98
- patterns
99
- .map { |f| FileNameResolver.call(f, model_name, table_name) }
100
- .map { |f| Dir.glob(f) }
101
- .flatten
102
- .each do |f|
103
- if FileAnnotator.call(f, info, position_key, options)
104
- annotated << f
105
- end
106
- end
107
- end
108
- rescue StandardError => e
109
- $stderr.puts "Unable to annotate #{file}: #{e.message}"
110
- $stderr.puts "\t" + e.backtrace.join("\n\t") if options[:trace]
44
+ related_files = RelatedFilesListBuilder.new(file, model_name, table_name, options).build
45
+ related_file_instructions = related_files.map do |f, position_key|
46
+ _instruction = FileAnnotatorInstruction.new(f, annotation, position_key, options)
111
47
  end
48
+ instructions.concat(related_file_instructions)
112
49
 
113
- annotated
50
+ instructions
114
51
  end
115
52
  end
116
53
  end
@@ -11,6 +11,8 @@ module AnnotateRb
11
11
  def call(options)
12
12
  model_files = []
13
13
 
14
+ # Note: This is currently broken as we don't set `is_rake` anywhere.
15
+ # It's an artifact from the old Annotate gem and how it did control flow.
14
16
  model_files = list_model_files_from_argument(options) if !options[:is_rake]
15
17
 
16
18
  return model_files if !model_files.empty?
@@ -30,7 +32,7 @@ module AnnotateRb
30
32
  rescue SystemCallError
31
33
  $stderr.puts "No models found in directory '#{options[:model_dir].join("', '")}'."
32
34
  $stderr.puts "Either specify models on the command line, or use the --model-dir option."
33
- $stderr.puts "Call 'annotate --help' for more info."
35
+ $stderr.puts "Call 'annotaterb --help' for more info."
34
36
  # exit 1 # TODO: Return exit code back to caller. Right now it messes up RSpec being able to run
35
37
  end
36
38
 
@@ -50,7 +52,7 @@ module AnnotateRb
50
52
 
51
53
  if model_files.size != specified_files.size
52
54
  $stderr.puts "The specified file could not be found in directory '#{options[:model_dir].join("', '")}'."
53
- $stderr.puts "Call 'annotate --help' for more info."
55
+ $stderr.puts "Call 'annotaterb --help' for more info."
54
56
  # exit 1 # TODO: Return exit code back to caller. Right now it messes up RSpec being able to run
55
57
  end
56
58