annotaterb 4.4.1 → 4.6.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 (45) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +43 -0
  3. data/README.md +29 -0
  4. data/VERSION +1 -1
  5. data/lib/annotate_rb/config_generator.rb +28 -0
  6. data/lib/annotate_rb/eager_loader.rb +6 -2
  7. data/lib/annotate_rb/model_annotator/annotated_file/generator.rb +92 -0
  8. data/lib/annotate_rb/model_annotator/annotated_file/updater.rb +46 -0
  9. data/lib/annotate_rb/model_annotator/annotated_file.rb +10 -0
  10. data/lib/annotate_rb/model_annotator/annotator.rb +2 -2
  11. data/lib/annotate_rb/model_annotator/file_name_resolver.rb +5 -0
  12. data/lib/annotate_rb/model_annotator/file_parser/annotation_finder.rb +103 -0
  13. data/lib/annotate_rb/model_annotator/file_parser/custom_parser.rb +217 -0
  14. data/lib/annotate_rb/model_annotator/file_parser/parsed_file.rb +94 -0
  15. data/lib/annotate_rb/model_annotator/file_parser/parsed_file_result.rb +54 -0
  16. data/lib/annotate_rb/model_annotator/file_parser.rb +12 -0
  17. data/lib/annotate_rb/model_annotator/model_class_getter.rb +7 -0
  18. data/lib/annotate_rb/model_annotator/model_files_getter.rb +4 -8
  19. data/lib/annotate_rb/model_annotator/model_wrapper.rb +11 -2
  20. data/lib/annotate_rb/model_annotator/pattern_getter.rb +2 -0
  21. data/lib/annotate_rb/model_annotator/related_files_list_builder.rb +3 -3
  22. data/lib/annotate_rb/model_annotator/single_file_annotation_remover.rb +10 -12
  23. data/lib/annotate_rb/model_annotator/single_file_annotator.rb +15 -8
  24. data/lib/annotate_rb/model_annotator/single_file_annotator_instruction.rb +1 -1
  25. data/lib/annotate_rb/model_annotator/single_file_remove_annotation_instruction.rb +1 -1
  26. data/lib/annotate_rb/model_annotator/zeitwerk_class_getter.rb +113 -0
  27. data/lib/annotate_rb/model_annotator.rb +3 -4
  28. data/lib/annotate_rb/options.rb +4 -0
  29. data/lib/annotate_rb/parser.rb +9 -3
  30. data/lib/annotate_rb/runner.rb +5 -4
  31. data/lib/annotate_rb/tasks/annotate_models_migrate.rake +5 -0
  32. data/lib/annotate_rb.rb +1 -0
  33. data/lib/generators/annotate_rb/config/USAGE +6 -0
  34. data/lib/generators/annotate_rb/config/config_generator.rb +15 -0
  35. data/lib/generators/annotate_rb/hook/USAGE +7 -0
  36. data/lib/generators/annotate_rb/hook/hook_generator.rb +15 -0
  37. data/lib/generators/annotate_rb/install/install_generator.rb +3 -4
  38. data/lib/generators/annotate_rb/update_config/USAGE +6 -0
  39. data/lib/generators/annotate_rb/update_config/update_config_generator.rb +15 -0
  40. metadata +20 -8
  41. data/lib/annotate_rb/model_annotator/annotation_pattern_generator.rb +0 -19
  42. data/lib/annotate_rb/model_annotator/file_builder.rb +0 -57
  43. data/lib/annotate_rb/model_annotator/file_components.rb +0 -81
  44. data/lib/annotate_rb/model_annotator/magic_comment_parser.rb +0 -32
  45. /data/lib/generators/annotate_rb/{install → hook}/templates/annotate_rb.rake +0 -0
@@ -0,0 +1,94 @@
1
+ # frozen_string_literal: true
2
+
3
+ module AnnotateRb
4
+ module ModelAnnotator
5
+ module FileParser
6
+ class ParsedFile
7
+ SKIP_ANNOTATION_STRING = "# -*- SkipSchemaAnnotations"
8
+
9
+ def initialize(file_content, new_annotations, options)
10
+ @file_content = file_content
11
+ @file_lines = @file_content.lines
12
+ @new_annotations = new_annotations
13
+ @options = options
14
+ end
15
+
16
+ def parse
17
+ @finder = AnnotationFinder.new(@file_content, @options[:wrapper_open], @options[:wrapper_close])
18
+ has_annotations = false
19
+
20
+ begin
21
+ @finder.run
22
+ has_annotations = @finder.annotated?
23
+ rescue AnnotationFinder::NoAnnotationFound => _e
24
+ end
25
+
26
+ annotations = if has_annotations
27
+ @file_lines[(@finder.annotation_start)..(@finder.annotation_end)].join
28
+ else
29
+ ""
30
+ end
31
+
32
+ @diff = AnnotationDiffGenerator.new(annotations, @new_annotations).generate
33
+ @file_parser = @finder.parser
34
+
35
+ has_skip_string = @file_parser.comments.any? { |comment, _lineno| comment.include?(SKIP_ANNOTATION_STRING) }
36
+ annotations_changed = @diff.changed?
37
+
38
+ has_leading_whitespace = false
39
+ has_trailing_whitespace = false
40
+
41
+ annotations_with_whitespace = if has_annotations
42
+ begin
43
+ annotation_start = @finder.annotation_start
44
+ annotation_end = @finder.annotation_end
45
+
46
+ if @file_lines[annotation_start - 1]&.strip&.empty?
47
+ annotation_start -= 1
48
+ has_leading_whitespace = true
49
+ end
50
+
51
+ if @file_lines[annotation_end + 1]&.strip&.empty?
52
+ annotation_end += 1
53
+ has_trailing_whitespace = true
54
+ end
55
+
56
+ @file_lines[annotation_start..annotation_end].join
57
+ end
58
+ else
59
+ ""
60
+ end
61
+
62
+ # :before or :after when it's set
63
+ annotation_position = nil
64
+
65
+ if has_annotations
66
+ const_declaration = @file_parser.starts.first
67
+
68
+ # If the file does not have any class or module declaration then const_declaration can be nil
69
+ _const, line_number = const_declaration
70
+
71
+ if line_number
72
+ annotation_position = if @finder.annotation_start < line_number
73
+ :before
74
+ else
75
+ :after
76
+ end
77
+ end
78
+ end
79
+
80
+ _result = ParsedFileResult.new(
81
+ has_annotations: has_annotations,
82
+ has_skip_string: has_skip_string,
83
+ annotations_changed: annotations_changed,
84
+ annotations: annotations,
85
+ annotations_with_whitespace: annotations_with_whitespace,
86
+ has_leading_whitespace: has_leading_whitespace,
87
+ has_trailing_whitespace: has_trailing_whitespace,
88
+ annotation_position: annotation_position
89
+ )
90
+ end
91
+ end
92
+ end
93
+ end
94
+ end
@@ -0,0 +1,54 @@
1
+ # frozen_string_literal: true
2
+
3
+ module AnnotateRb
4
+ module ModelAnnotator
5
+ module FileParser
6
+ class ParsedFileResult
7
+ def initialize(
8
+ has_annotations:,
9
+ has_skip_string:,
10
+ annotations_changed:,
11
+ annotations:,
12
+ annotations_with_whitespace:,
13
+ has_leading_whitespace:,
14
+ has_trailing_whitespace:,
15
+ annotation_position:
16
+ )
17
+ @has_annotations = has_annotations
18
+ @has_skip_string = has_skip_string
19
+ @annotations_changed = annotations_changed
20
+ @annotations = annotations
21
+ @annotations_with_whitespace = annotations_with_whitespace
22
+ @has_leading_whitespace = has_leading_whitespace
23
+ @has_trailing_whitespace = has_trailing_whitespace
24
+ @annotation_position = annotation_position
25
+ end
26
+
27
+ attr_reader :annotations, :annotation_position
28
+
29
+ # Returns annotations with new line before and after if they exist
30
+ attr_reader :annotations_with_whitespace
31
+
32
+ def annotations_changed?
33
+ @annotations_changed
34
+ end
35
+
36
+ def has_annotations?
37
+ @has_annotations
38
+ end
39
+
40
+ def has_skip_string?
41
+ @has_skip_string
42
+ end
43
+
44
+ def has_leading_whitespace?
45
+ @has_leading_whitespace
46
+ end
47
+
48
+ def has_trailing_whitespace?
49
+ @has_trailing_whitespace
50
+ end
51
+ end
52
+ end
53
+ end
54
+ end
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ module AnnotateRb
4
+ module ModelAnnotator
5
+ module FileParser
6
+ autoload :AnnotationFinder, "annotate_rb/model_annotator/file_parser/annotation_finder"
7
+ autoload :CustomParser, "annotate_rb/model_annotator/file_parser/custom_parser"
8
+ autoload :ParsedFile, "annotate_rb/model_annotator/file_parser/parsed_file"
9
+ autoload :ParsedFileResult, "annotate_rb/model_annotator/file_parser/parsed_file_result"
10
+ end
11
+ end
12
+ end
@@ -8,6 +8,13 @@ module AnnotateRb
8
8
  # Check for namespaced models in subdirectories as well as models
9
9
  # in subdirectories without namespacing.
10
10
  def call(file, options)
11
+ use_zeitwerk = defined?(::Rails) && ::Rails.try(:autoloaders).try(:zeitwerk_enabled?)
12
+
13
+ if use_zeitwerk
14
+ klass = ZeitwerkClassGetter.call(file, options)
15
+ return klass if klass
16
+ end
17
+
11
18
  model_path = file.gsub(/\.rb$/, "")
12
19
  options[:model_dir].each { |dir| model_path = model_path.gsub(/^#{dir}/, "").gsub(/^\//, "") }
13
20
 
@@ -9,13 +9,9 @@ module AnnotateRb
9
9
  # of model files from root dir. Otherwise we take all the model files
10
10
  # in the model_dir directory.
11
11
  def call(options)
12
- model_files = []
12
+ model_files = list_model_files_from_argument(options)
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.
16
- model_files = list_model_files_from_argument(options) if !options[:is_rake]
17
-
18
- return model_files if !model_files.empty?
14
+ return model_files if model_files.any?
19
15
 
20
16
  options[:model_dir].each do |dir|
21
17
  Dir.chdir(dir) do
@@ -41,9 +37,9 @@ module AnnotateRb
41
37
  private
42
38
 
43
39
  def list_model_files_from_argument(options)
44
- return [] if ARGV.empty?
40
+ return [] if options.get_state(:working_args).empty?
45
41
 
46
- specified_files = ARGV.map { |file| File.expand_path(file) }
42
+ specified_files = options.get_state(:working_args).map { |file| File.expand_path(file) }
47
43
 
48
44
  model_files = options[:model_dir].flat_map do |dir|
49
45
  absolute_dir_path = File.expand_path(dir)
@@ -6,7 +6,7 @@ module AnnotateRb
6
6
  # Should be the wrapper for an ActiveRecord model that serves as the source of truth of the model
7
7
  # of the model that we're annotating
8
8
 
9
- def initialize(klass, options = {})
9
+ def initialize(klass, options)
10
10
  @klass = klass
11
11
  @options = options
12
12
  end
@@ -115,7 +115,16 @@ module AnnotateRb
115
115
 
116
116
  # Try to search the table without prefix
117
117
  table_name_without_prefix = table_name.to_s.sub(@klass.table_name_prefix, "")
118
- @klass.connection.indexes(table_name_without_prefix)
118
+ begin
119
+ @klass.connection.indexes(table_name_without_prefix)
120
+ rescue ActiveRecord::StatementInvalid => _e
121
+ # Mysql2 adapter behaves differently than Sqlite3 and Postgres adapter.
122
+ # If `table_name_without_prefix` does not exist, Mysql2 will raise,
123
+ # the other adapters will return an empty array.
124
+ #
125
+ # See: https://github.com/rails/rails/issues/51205
126
+ []
127
+ end
119
128
  end
120
129
 
121
130
  def with_comments?
@@ -142,6 +142,8 @@ module AnnotateRb
142
142
  File.join(root_directory, FilePatterns::FACTORY_BOT_SPEC_DIR, "%TABLE_NAME%.rb"), # (new style)
143
143
  File.join(root_directory, FilePatterns::FACTORY_BOT_TEST_DIR, "%PLURALIZED_MODEL_NAME%.rb"), # (new style)
144
144
  File.join(root_directory, FilePatterns::FACTORY_BOT_SPEC_DIR, "%PLURALIZED_MODEL_NAME%.rb"), # (new style)
145
+ File.join(root_directory, FilePatterns::FACTORY_BOT_TEST_DIR, "%PLURALIZED_MODEL_NAME%_factory.rb"), # (new style)
146
+ File.join(root_directory, FilePatterns::FACTORY_BOT_SPEC_DIR, "%PLURALIZED_MODEL_NAME%_factory.rb"), # (new style)
145
147
  File.join(root_directory, FilePatterns::FABRICATORS_TEST_DIR, "%MODEL_NAME%_fabricator.rb"),
146
148
  File.join(root_directory, FilePatterns::FABRICATORS_SPEC_DIR, "%MODEL_NAME%_fabricator.rb")
147
149
  ]
@@ -24,10 +24,10 @@ module AnnotateRb
24
24
  add_related_scaffold_files if !@options[:exclude_scaffolds]
25
25
  add_related_controller_files if !@options[:exclude_controllers]
26
26
  add_related_helper_files if !@options[:exclude_helpers]
27
- add_related_admin_files if !@options[:active_admin]
28
- add_additional_file_patterns if !@options[:additional_file_patterns].present?
27
+ add_related_admin_files if @options[:active_admin]
28
+ add_additional_file_patterns if @options[:additional_file_patterns].present?
29
29
 
30
- @list
30
+ @list.uniq
31
31
  end
32
32
 
33
33
  private
@@ -12,20 +12,18 @@ module AnnotateRb
12
12
  return false unless File.exist?(file_name)
13
13
  old_content = File.read(file_name)
14
14
 
15
- file_components = FileComponents.new(old_content, "", options)
16
-
17
- return false if file_components.has_skip_string?
18
- # TODO: Uncomment below after tests are fixed
19
- # return false if !file_components.has_annotations?
20
-
21
- wrapper_open = if options[:wrapper_open]
22
- "# #{options[:wrapper_open]}\n"
23
- else
24
- ""
15
+ begin
16
+ parsed_file = FileParser::ParsedFile.new(old_content, "", options).parse
17
+ rescue FileParser::AnnotationFinder::MalformedAnnotation => e
18
+ warn "Unable to process #{file_name}: #{e.message}"
19
+ warn "\t" + e.backtrace.join("\n\t") if @options[:trace]
20
+ return false
25
21
  end
26
22
 
27
- generated_pattern = AnnotationPatternGenerator.call(options)
28
- updated_file_content = old_content.sub!(/(#{wrapper_open})?#{generated_pattern}/, "")
23
+ return false if !parsed_file.has_annotations?
24
+ return false if parsed_file.has_skip_string?
25
+
26
+ updated_file_content = old_content.sub(parsed_file.annotations_with_whitespace, "")
29
27
 
30
28
  File.open(file_name, "wb") { |f| f.puts updated_file_content }
31
29
 
@@ -21,22 +21,29 @@ module AnnotateRb
21
21
  # :position_in_*<Symbol>:: where to place the annotated section in fixture or model file,
22
22
  # :before, :top, :after or :bottom. Default is :before.
23
23
  #
24
- def call(file_name, annotation, annotation_position, options = {})
24
+ def call(file_name, annotation, annotation_position, options)
25
25
  return false unless File.exist?(file_name)
26
26
  old_content = File.read(file_name)
27
27
 
28
- file_components = FileComponents.new(old_content, annotation, options)
29
- builder = FileBuilder.new(file_components, annotation_position, options)
28
+ begin
29
+ parsed_file = FileParser::ParsedFile.new(old_content, annotation, options).parse
30
+ rescue FileParser::AnnotationFinder::MalformedAnnotation => e
31
+ warn "Unable to process #{file_name}: #{e.message}"
32
+ warn "\t" + e.backtrace.join("\n\t") if @options[:trace]
33
+ return false
34
+ end
30
35
 
31
- return false if file_components.has_skip_string?
32
- return false if !file_components.annotations_changed? && !options[:force]
36
+ return false if parsed_file.has_skip_string?
37
+ return false if !parsed_file.annotations_changed? && !options[:force]
33
38
 
34
39
  abort "AnnotateRb error. #{file_name} needs to be updated, but annotaterb was run with `--frozen`." if options[:frozen]
35
40
 
36
- updated_file_content = if !file_components.has_annotations? || options[:force]
37
- builder.generate_content_with_new_annotations
41
+ updated_file_content = if !parsed_file.has_annotations?
42
+ AnnotatedFile::Generator.new(old_content, annotation, annotation_position, options).generate
43
+ elsif options[:force]
44
+ AnnotatedFile::Generator.new(old_content, annotation, annotation_position, options).generate
38
45
  else
39
- builder.update_existing_annotations
46
+ AnnotatedFile::Updater.new(old_content, annotation, annotation_position, options).update
40
47
  end
41
48
 
42
49
  File.open(file_name, "wb") { |f| f.puts updated_file_content }
@@ -4,7 +4,7 @@ module AnnotateRb
4
4
  module ModelAnnotator
5
5
  # A plain old Ruby object (PORO) that contains all necessary information for SingleFileAnnotator
6
6
  class SingleFileAnnotatorInstruction
7
- def initialize(file, annotation, position, options = {})
7
+ def initialize(file, annotation, position, options)
8
8
  @file = file # Path to file
9
9
  @annotation = annotation # Annotation string
10
10
  @position = position # Position in the file where to write the annotation to
@@ -4,7 +4,7 @@ module AnnotateRb
4
4
  module ModelAnnotator
5
5
  # A plain old Ruby object (PORO) that contains all necessary information for SingleFileAnnotationRemover
6
6
  class SingleFileRemoveAnnotationInstruction
7
- def initialize(file, options = {})
7
+ def initialize(file, options)
8
8
  @file = file # Path to file
9
9
  @options = options
10
10
  end
@@ -0,0 +1,113 @@
1
+ # frozen_string_literal: true
2
+
3
+ module AnnotateRb
4
+ module ModelAnnotator
5
+ class ZeitwerkClassGetter
6
+ class << self
7
+ def call(file, options)
8
+ new(file, options).call
9
+ end
10
+ end
11
+
12
+ def initialize(file, options)
13
+ @file = file
14
+ @options = options
15
+ end
16
+
17
+ # @return [Constant, nil] Attempts to return the model class constant (e.g. User) defined in the model file
18
+ # can return `nil` if the file does not define the constant.
19
+ def call
20
+ return unless defined?(::Zeitwerk)
21
+
22
+ @absolute_file_path = File.expand_path(@file)
23
+ loader = ::Rails.autoloaders.main
24
+
25
+ if supports_cpath?
26
+ constant_using_cpath(loader)
27
+ else
28
+ constant(loader)
29
+ end
30
+ end
31
+
32
+ private
33
+
34
+ def constant(loader)
35
+ root_dirs = loader.dirs(namespaces: true) # or `root_dirs = loader.root_dirs` with zeitwerk < 2.6.1
36
+ expanded_file = @absolute_file_path
37
+
38
+ # root_dir: "/home/dummyapp/app/models"
39
+ root_dir, namespace = root_dirs.find do |dir, _namespace|
40
+ expanded_file.start_with?(dir)
41
+ end
42
+
43
+ # expanded_file: "/home/dummyapp/app/models/collapsed/example/test_model.rb"
44
+ # filepath_relative_to_root_dir: "/collapsed/example/test_model.rb"
45
+ _, filepath_relative_to_root_dir = expanded_file.split(root_dir)
46
+
47
+ # Remove leading / and the .rb extension
48
+ filepath_relative_to_root_dir = filepath_relative_to_root_dir[1..].sub(/\.rb$/, "")
49
+
50
+ # once we have the filepath_relative_to_root_dir, we need to see if it
51
+ # falls within one of our Zeitwerk "collapsed" paths.
52
+ if loader.collapse.any? { |path| path.include?(root_dir) && file.include?(path.split(root_dir)[1]) }
53
+ # if the file is within a collapsed path, we then need to, for each
54
+ # collapsed path, remove the root dir
55
+ collapsed = loader.collapse.map { |path| path.split(root_dir)[1].sub(/^\//, "") }.to_set
56
+
57
+ collapsed.each do |collapse|
58
+ # next, we split the collapsed directory, e.g. `domain_name/models`, by
59
+ # slash, and discard the domain_name
60
+ _, *collapsed_namespace = collapse.split("/")
61
+
62
+ # if there are any collapsed namespaces, e.g. `models`, we then remove
63
+ # that from `filepath_relative_to_root_dir`.
64
+ #
65
+ # This would result in:
66
+ #
67
+ # previous filepath_relative_to_root_dir: domain_name/models/model_name
68
+ # new filepath_relative_to_root_dir: domain_name/model_name
69
+ if collapsed_namespace.any?
70
+ filepath_relative_to_root_dir.sub!("/#{collapsed_namespace.last}", "")
71
+ end
72
+ end
73
+ end
74
+
75
+ camelize = loader.inflector.camelize(filepath_relative_to_root_dir, nil)
76
+ namespace.const_get(camelize)
77
+ rescue NameError => e
78
+ warn e
79
+ nil
80
+ end
81
+
82
+ def constant_using_cpath(loader)
83
+ begin
84
+ constant = loader.cpath_expected_at(@absolute_file_path)
85
+ rescue ::Zeitwerk::Error => e
86
+ # Raises when file does not exist
87
+ warn "Zeitwerk unable to find file #{@file}, error:\n#{e.message}"
88
+ return
89
+ end
90
+
91
+ begin
92
+ # This uses ActiveSupport::Inflector.constantize
93
+ klass = constant.constantize
94
+ rescue NameError => e
95
+ warn e
96
+ return
97
+ end
98
+
99
+ klass
100
+ end
101
+
102
+ def supports_cpath?
103
+ @supports_cpath ||=
104
+ begin
105
+ current_version = ::Gem::Version.new(::Zeitwerk::VERSION)
106
+ required_version = ::Gem::Version.new("2.6.9")
107
+
108
+ current_version >= required_version
109
+ end
110
+ end
111
+ end
112
+ end
113
+ end
@@ -7,7 +7,6 @@ module AnnotateRb
7
7
  autoload :BadModelFileError, "annotate_rb/model_annotator/bad_model_file_error"
8
8
  autoload :FileNameResolver, "annotate_rb/model_annotator/file_name_resolver"
9
9
  autoload :SingleFileAnnotationRemover, "annotate_rb/model_annotator/single_file_annotation_remover"
10
- autoload :AnnotationPatternGenerator, "annotate_rb/model_annotator/annotation_pattern_generator"
11
10
  autoload :ModelClassGetter, "annotate_rb/model_annotator/model_class_getter"
12
11
  autoload :ModelFilesGetter, "annotate_rb/model_annotator/model_files_getter"
13
12
  autoload :SingleFileAnnotator, "annotate_rb/model_annotator/single_file_annotator"
@@ -22,10 +21,10 @@ module AnnotateRb
22
21
  autoload :SingleFileRemoveAnnotationInstruction, "annotate_rb/model_annotator/single_file_remove_annotation_instruction"
23
22
  autoload :AnnotationDiffGenerator, "annotate_rb/model_annotator/annotation_diff_generator"
24
23
  autoload :AnnotationDiff, "annotate_rb/model_annotator/annotation_diff"
25
- autoload :FileBuilder, "annotate_rb/model_annotator/file_builder"
26
- autoload :MagicCommentParser, "annotate_rb/model_annotator/magic_comment_parser"
27
- autoload :FileComponents, "annotate_rb/model_annotator/file_components"
28
24
  autoload :ProjectAnnotator, "annotate_rb/model_annotator/project_annotator"
29
25
  autoload :ProjectAnnotationRemover, "annotate_rb/model_annotator/project_annotation_remover"
26
+ autoload :AnnotatedFile, "annotate_rb/model_annotator/annotated_file"
27
+ autoload :FileParser, "annotate_rb/model_annotator/file_parser"
28
+ autoload :ZeitwerkClassGetter, "annotate_rb/model_annotator/zeitwerk_class_getter"
30
29
  end
31
30
  end
@@ -210,5 +210,9 @@ module AnnotateRb
210
210
  def get_state(key)
211
211
  @state[key]
212
212
  end
213
+
214
+ def print
215
+ # TODO: prints options and state
216
+ end
213
217
  end
214
218
  end
@@ -3,7 +3,7 @@ require "optparse"
3
3
  module AnnotateRb
4
4
  # Class for handling command line arguments
5
5
  class Parser # rubocop:disable Metrics/ClassLength
6
- def self.parse(args, existing_options = {})
6
+ def self.parse(args, existing_options)
7
7
  new(args, existing_options).parse
8
8
  end
9
9
 
@@ -35,11 +35,11 @@ module AnnotateRb
35
35
  }.freeze
36
36
 
37
37
  def initialize(args, existing_options)
38
- @args = args
38
+ @args = args.clone
39
39
  base_options = DEFAULT_OPTIONS.dup
40
40
  @options = base_options.merge(existing_options)
41
41
  @commands = []
42
- @options[:original_args] = args.dup
42
+ @options[:original_args] = args.clone
43
43
  end
44
44
 
45
45
  def parse
@@ -52,6 +52,12 @@ module AnnotateRb
52
52
  @options
53
53
  end
54
54
 
55
+ def remaining_args
56
+ # `@args` gets modified throughout the lifecycle of this class.
57
+ # It starts as a shallow clone of ARGV, then arguments matching commands and options are removed in #parse
58
+ @args
59
+ end
60
+
55
61
  private
56
62
 
57
63
  def parse_command(args)
@@ -9,14 +9,15 @@ module AnnotateRb
9
9
  end
10
10
 
11
11
  def run(args)
12
- _original_args = args.dup
13
-
14
12
  config_file_options = ConfigLoader.load_config
15
- parsed_options = Parser.parse(args)
13
+ parser = Parser.new(args, {})
14
+
15
+ parsed_options = parser.parse
16
+ remaining_args = parser.remaining_args
16
17
 
17
18
  options = config_file_options.merge(parsed_options)
18
19
 
19
- @options = Options.from(options, {})
20
+ @options = Options.from(options, {working_args: remaining_args})
20
21
  AnnotateRb::RakeBootstrapper.call(@options)
21
22
 
22
23
  if @options[:command]
@@ -6,6 +6,11 @@
6
6
 
7
7
  # Migration tasks are tasks that we'll "hook" into
8
8
  migration_tasks = %w[db:migrate db:migrate:up db:migrate:down db:migrate:reset db:migrate:redo db:rollback]
9
+
10
+ # Support for data_migrate gem (https://github.com/ilyakatz/data-migrate)
11
+ migration_tasks_with_data = migration_tasks.map { |task| "#{task}:with_data" }
12
+ migration_tasks += migration_tasks_with_data
13
+
9
14
  if defined?(Rails::Application) && Rails.version.split(".").first.to_i >= 6
10
15
  require "active_record"
11
16
 
data/lib/annotate_rb.rb CHANGED
@@ -22,6 +22,7 @@ require_relative "annotate_rb/eager_loader"
22
22
  require_relative "annotate_rb/rake_bootstrapper"
23
23
  require_relative "annotate_rb/config_finder"
24
24
  require_relative "annotate_rb/config_loader"
25
+ require_relative "annotate_rb/config_generator"
25
26
 
26
27
  module AnnotateRb
27
28
  end
@@ -0,0 +1,6 @@
1
+ Description:
2
+ Generates a default configuration file, `.annotaterb.yml` in your
3
+ Rails app project root.
4
+
5
+ Example:
6
+ `rails generate annotate_rb:config`
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "annotate_rb"
4
+
5
+ module AnnotateRb
6
+ module Generators
7
+ class ConfigGenerator < ::Rails::Generators::Base
8
+ def generate_config
9
+ create_file ::AnnotateRb::ConfigFinder::DOTFILE do
10
+ ::AnnotateRb::ConfigGenerator.default_config_yml
11
+ end
12
+ end
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,7 @@
1
+ Description:
2
+ Adds a rake task into your Rails app's lib/tasks directory, that
3
+ automatically annotates models when you do a db:migrate in
4
+ development mode.
5
+
6
+ Example:
7
+ `rails generate annotate_rb:hook`
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "annotate_rb"
4
+
5
+ module AnnotateRb
6
+ module Generators
7
+ class HookGenerator < ::Rails::Generators::Base
8
+ source_root File.expand_path("templates", __dir__)
9
+
10
+ def copy_hook_file
11
+ copy_file "annotate_rb.rake", "lib/tasks/annotate_rb.rake"
12
+ end
13
+ end
14
+ end
15
+ end
@@ -5,10 +5,9 @@ require "annotate_rb"
5
5
  module AnnotateRb
6
6
  module Generators
7
7
  class InstallGenerator < ::Rails::Generators::Base
8
- source_root File.expand_path("templates", __dir__)
9
-
10
- def copy_task
11
- copy_file "annotate_rb.rake", "lib/tasks/annotate_rb.rake"
8
+ def install_hook_and_generate_defaults
9
+ generate "annotate_rb:hook"
10
+ generate "annotate_rb:config"
12
11
  end
13
12
  end
14
13
  end
@@ -0,0 +1,6 @@
1
+ Description:
2
+ Appends to .annotaterb.yml any missing default configuration
3
+ key-value pairs.
4
+
5
+ Example:
6
+ `rails generate annotate_rb:update_config`
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "annotate_rb"
4
+
5
+ module AnnotateRb
6
+ module Generators
7
+ class UpdateConfigGenerator < ::Rails::Generators::Base
8
+ def generate_config
9
+ insert_into_file ::AnnotateRb::ConfigFinder::DOTFILE do
10
+ ::AnnotateRb::ConfigGenerator.unset_config_defaults
11
+ end
12
+ end
13
+ end
14
+ end
15
+ end