annotaterb 4.0.0.beta.1 → 4.1.0

Sign up to get free protection for your applications and to get access to all the features.
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