annotaterb 4.0.0.beta.1

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 (49) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +0 -0
  3. data/LICENSE.txt +55 -0
  4. data/README.md +91 -0
  5. data/VERSION +1 -0
  6. data/exe/annotaterb +21 -0
  7. data/lib/annotate_rb/active_record_patch.rb +9 -0
  8. data/lib/annotate_rb/commands/annotate_models.rb +22 -0
  9. data/lib/annotate_rb/commands/annotate_routes.rb +19 -0
  10. data/lib/annotate_rb/commands/print_help.rb +16 -0
  11. data/lib/annotate_rb/commands/print_version.rb +12 -0
  12. data/lib/annotate_rb/commands.rb +10 -0
  13. data/lib/annotate_rb/config_finder.rb +21 -0
  14. data/lib/annotate_rb/config_loader.rb +63 -0
  15. data/lib/annotate_rb/core.rb +23 -0
  16. data/lib/annotate_rb/eager_loader.rb +23 -0
  17. data/lib/annotate_rb/env.rb +30 -0
  18. data/lib/annotate_rb/model_annotator/annotation_pattern_generator.rb +19 -0
  19. data/lib/annotate_rb/model_annotator/annotator.rb +74 -0
  20. data/lib/annotate_rb/model_annotator/bad_model_file_error.rb +11 -0
  21. data/lib/annotate_rb/model_annotator/constants.rb +22 -0
  22. data/lib/annotate_rb/model_annotator/file_annotation_remover.rb +25 -0
  23. data/lib/annotate_rb/model_annotator/file_annotator.rb +79 -0
  24. data/lib/annotate_rb/model_annotator/file_name_resolver.rb +16 -0
  25. data/lib/annotate_rb/model_annotator/file_patterns.rb +129 -0
  26. data/lib/annotate_rb/model_annotator/helper.rb +54 -0
  27. data/lib/annotate_rb/model_annotator/model_class_getter.rb +63 -0
  28. data/lib/annotate_rb/model_annotator/model_file_annotator.rb +118 -0
  29. data/lib/annotate_rb/model_annotator/model_files_getter.rb +62 -0
  30. data/lib/annotate_rb/model_annotator/pattern_getter.rb +27 -0
  31. data/lib/annotate_rb/model_annotator/schema_info.rb +480 -0
  32. data/lib/annotate_rb/model_annotator.rb +20 -0
  33. data/lib/annotate_rb/options.rb +204 -0
  34. data/lib/annotate_rb/parser.rb +385 -0
  35. data/lib/annotate_rb/rake_bootstrapper.rb +34 -0
  36. data/lib/annotate_rb/route_annotator/annotation_processor.rb +56 -0
  37. data/lib/annotate_rb/route_annotator/annotator.rb +40 -0
  38. data/lib/annotate_rb/route_annotator/base_processor.rb +104 -0
  39. data/lib/annotate_rb/route_annotator/header_generator.rb +113 -0
  40. data/lib/annotate_rb/route_annotator/helper.rb +104 -0
  41. data/lib/annotate_rb/route_annotator/removal_processor.rb +40 -0
  42. data/lib/annotate_rb/route_annotator.rb +12 -0
  43. data/lib/annotate_rb/runner.rb +34 -0
  44. data/lib/annotate_rb/tasks/annotate_models_migrate.rake +30 -0
  45. data/lib/annotate_rb.rb +30 -0
  46. data/lib/generators/annotate_rb/USAGE +4 -0
  47. data/lib/generators/annotate_rb/install_generator.rb +15 -0
  48. data/lib/generators/annotate_rb/templates/auto_annotate_models.rake +7 -0
  49. metadata +96 -0
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 4ca3a25c118bfa3f5910e4fe455de3d9333c353d409f434cffde12694f87cff5
4
+ data.tar.gz: 417dcf943056ec0f44ba7eb973c3ad2dc99cb435315d41a13d1fc9f53bb87b9e
5
+ SHA512:
6
+ metadata.gz: 2d34ca80e5686ef6cadfe0a2624aecd891fde3628653e68fa8b86613ac6382a4d7bdcf8e0c7504988dd653023be3d1190c5a072d46890b9c874ecaf1abbc20f7
7
+ data.tar.gz: 2ae8ca4a18c83437b379f7823291c094a87db9465742bf2c1c94c99fcfe1cb833f894367774a44ff2a9e7c8f41f9670f8baeedff1d5832bbbf2c49a6ced8ac80
data/CHANGELOG.md ADDED
File without changes
data/LICENSE.txt ADDED
@@ -0,0 +1,55 @@
1
+ You can redistribute it and/or modify it under either the terms of the
2
+ 2-clause BSDL (see the file BSDL), or the conditions below:
3
+
4
+ 1. You may make and give away verbatim copies of the source form of the
5
+ software without restriction, provided that you duplicate all of the
6
+ original copyright notices and associated disclaimers.
7
+
8
+ 2. You may modify your copy of the software in any way, provided that
9
+ you do at least ONE of the following:
10
+
11
+ a) place your modifications in the Public Domain or otherwise
12
+ make them Freely Available, such as by posting said
13
+ modifications to Usenet or an equivalent medium, or by allowing
14
+ the author to include your modifications in the software.
15
+
16
+ b) use the modified software only within your corporation or
17
+ organization.
18
+
19
+ c) give non-standard binaries non-standard names, with
20
+ instructions on where to get the original software distribution.
21
+
22
+ d) make other distribution arrangements with the author.
23
+
24
+ 3. You may distribute the software in object code or binary form,
25
+ provided that you do at least ONE of the following:
26
+
27
+ a) distribute the binaries and library files of the software,
28
+ together with instructions (in the manual page or equivalent)
29
+ on where to get the original distribution.
30
+
31
+ b) accompany the distribution with the machine-readable source of
32
+ the software.
33
+
34
+ c) give non-standard binaries non-standard names, with
35
+ instructions on where to get the original software distribution.
36
+
37
+ d) make other distribution arrangements with the author.
38
+
39
+ 4. You may modify and include the part of the software into any other
40
+ software (possibly commercial). But some files in the distribution
41
+ are not written by the author, so that they are not under these terms.
42
+
43
+ For the list of those files and their copying conditions, see the
44
+ file LEGAL.
45
+
46
+ 5. The scripts and library files supplied as input to or produced as
47
+ output from the software do not automatically fall under the
48
+ copyright of the software, but belong to whomever generated them,
49
+ and may be sold commercially, and may be aggregated with this
50
+ software.
51
+
52
+ 6. THIS SOFTWARE IS PROVIDED "AS IS" AND WITHOUT ANY EXPRESS OR
53
+ IMPLIED WARRANTIES, INCLUDING, WITHOUT LIMITATION, THE IMPLIED
54
+ WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
55
+ PURPOSE.
data/README.md ADDED
@@ -0,0 +1,91 @@
1
+ ## AnnotateRb
2
+ ### forked from the [Annotate aka AnnotateModels gem](https://github.com/ctran/annotate_models)
3
+
4
+ ----------
5
+ [![CI](https://github.com/drwl/annotaterb/actions/workflows/ci.yml/badge.svg)](https://github.com/drwl/annotaterb/actions/workflows/ci.yml)
6
+
7
+ Adds comments summarizing the model schema or routes in your:
8
+
9
+ - ActiveRecord models
10
+ - Fixture files
11
+ - Tests and Specs
12
+ - FactoryBot factories
13
+ - `routes.rb` file (for Rails projects)
14
+
15
+ The schema comment looks like this:
16
+
17
+ ```ruby
18
+ # == Schema Information
19
+ #
20
+ # Table name: tasks
21
+ #
22
+ # id :integer not null, primary key
23
+ # content :string
24
+ # count :integer
25
+ # status :boolean
26
+ # created_at :datetime not null
27
+ # updated_at :datetime not null
28
+ #
29
+ class Task < ApplicationRecord
30
+ ...
31
+ ```
32
+ ----------
33
+ ## Installation
34
+
35
+ ```sh
36
+ $ gem install annotaterb
37
+ ```
38
+
39
+ Or install it into your Rails project through the Gemfile:
40
+
41
+ ```rb
42
+ group :development do
43
+ ...
44
+
45
+ gem "annotaterb"
46
+
47
+ ...
48
+ ```
49
+
50
+ ### Automatically annotate models
51
+ For Rails projects, model files can get automatically annotated after migration tasks. To do this, run the following command:
52
+
53
+ ```sh
54
+ $ bin/rails g annotate_rb:install
55
+ ```
56
+
57
+ This will copy a rake task into your Rails project's `lib/tasks` directory that will hook into the Rails project rake tasks, automatically running AnnotateRb after database migration rake tasks.
58
+
59
+ ## Migrating from the annotate gem
60
+
61
+ Add steps for migrating from annotate gem.
62
+
63
+ ## Usage
64
+
65
+ AnnotateRb has a CLI that you can use to add or remove annotations.
66
+
67
+ ```sh
68
+ # To show the CLI options
69
+ $ bundle exec annotaterb
70
+ ```
71
+
72
+ ## Configuration
73
+
74
+
75
+ ### How to skip annotating a particular model
76
+ If you want to always skip annotations on a particular model, add this string
77
+ anywhere in the file:
78
+
79
+ # -*- SkipSchemaAnnotations
80
+
81
+ ## Sorting
82
+
83
+ By default, columns will be sorted in database order (i.e. the order in which
84
+ migrations were run).
85
+
86
+ If you prefer to sort alphabetically so that the results of annotation are
87
+ consistent regardless of what order migrations are executed in, use `--sort`.
88
+
89
+ ## License
90
+
91
+ Released under the same license as Ruby. No Support. No Warranty.
data/VERSION ADDED
@@ -0,0 +1 @@
1
+ 4.0.0.beta.1
data/exe/annotaterb ADDED
@@ -0,0 +1,21 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ unless File.exist?('./Rakefile') || File.exist?('./Gemfile')
5
+ abort 'Please run annotaterb from the root of the project.'
6
+ end
7
+
8
+ begin
9
+ require 'bundler'
10
+ Bundler.setup
11
+ rescue StandardError
12
+ end
13
+
14
+ $LOAD_PATH.unshift("#{__dir__}/../lib")
15
+
16
+ require 'annotate_rb'
17
+
18
+ exit_status = ::AnnotateRb::Runner.run(ARGV)
19
+
20
+ # TODO: Return exit status
21
+ # exit exit_status
@@ -0,0 +1,9 @@
1
+ # monkey patches
2
+
3
+ module ::ActiveRecord
4
+ class Base
5
+ def self.method_missing(_name, *_args)
6
+ # ignore this, so unknown/unloaded macros won't cause parsing to fail
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,22 @@
1
+ # frozen_string_literal: true
2
+
3
+ module AnnotateRb
4
+ module Commands
5
+ class AnnotateModels
6
+ def call(options)
7
+ puts "Annotating models"
8
+
9
+ if options[:debug]
10
+ puts "Running with debug mode, options:"
11
+ pp options.to_h
12
+ end
13
+
14
+ # Eager load Models when we're annotating models
15
+ AnnotateRb::EagerLoader.call(options)
16
+
17
+ AnnotateRb::ModelAnnotator::Annotator.send(options[:target_action], options)
18
+ end
19
+ end
20
+ end
21
+ end
22
+
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ module AnnotateRb
4
+ module Commands
5
+ class AnnotateRoutes
6
+ def call(options)
7
+ puts "Annotating routes"
8
+
9
+ if options[:debug]
10
+ puts "Running with debug mode, options:"
11
+ pp options.to_h
12
+ end
13
+
14
+ AnnotateRb::RouteAnnotator::Annotator.send(options[:target_action], options)
15
+ end
16
+ end
17
+ end
18
+ end
19
+
@@ -0,0 +1,16 @@
1
+ # frozen_string_literal: true
2
+
3
+ module AnnotateRb
4
+ module Commands
5
+ class PrintHelp
6
+ def initialize(parser)
7
+ @parser = parser
8
+ end
9
+
10
+ def call(_options)
11
+ puts @parser.help
12
+ end
13
+ end
14
+ end
15
+ end
16
+
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ module AnnotateRb
4
+ module Commands
5
+ class PrintVersion
6
+ def call(_options)
7
+ puts "AnnotateRb v#{Core.version}"
8
+ end
9
+ end
10
+ end
11
+ end
12
+
@@ -0,0 +1,10 @@
1
+ # frozen_string_literal: true
2
+
3
+ module AnnotateRb
4
+ module Commands
5
+ autoload :PrintVersion, 'annotate_rb/commands/print_version'
6
+ autoload :PrintHelp, 'annotate_rb/commands/print_help'
7
+ autoload :AnnotateModels, 'annotate_rb/commands/annotate_models'
8
+ autoload :AnnotateRoutes, 'annotate_rb/commands/annotate_routes'
9
+ end
10
+ end
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ module AnnotateRb
4
+ class ConfigFinder
5
+ DOTFILE = '.annotaterb.yml'
6
+
7
+ class << self
8
+ def find_project_root
9
+ # We should expect this method to be called from a Rails project root and returning it
10
+ # e.g. "/Users/drwl/personal/annotaterb/dummyapp"
11
+ Dir.pwd
12
+ end
13
+
14
+ def find_project_dotfile
15
+ file_path = File.expand_path(DOTFILE, find_project_root)
16
+
17
+ return file_path if File.exist?(file_path)
18
+ end
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,63 @@
1
+ # frozen_string_literal: true
2
+
3
+ module AnnotateRb
4
+ # Raised when a configuration file is not found.
5
+ class ConfigNotFoundError < StandardError
6
+ end
7
+
8
+ class ConfigLoader
9
+ class << self
10
+ def load_config
11
+ config_path = ConfigFinder.find_project_dotfile
12
+
13
+ if config_path
14
+ load_yaml_configuration(config_path)
15
+ else
16
+ {}
17
+ end
18
+ end
19
+
20
+ # Method from Rubocop::ConfigLoader
21
+ def load_yaml_configuration(absolute_path)
22
+ file_contents = read_file(absolute_path)
23
+
24
+ hash = yaml_safe_load(file_contents, absolute_path) || {}
25
+
26
+ # TODO: Print config if debug flag/option is set
27
+
28
+ raise(TypeError, "Malformed configuration in #{absolute_path}") unless hash.is_a?(Hash)
29
+
30
+ hash
31
+ end
32
+
33
+ # Read the specified file, or exit with a friendly, concise message on
34
+ # stderr. Care is taken to use the standard OS exit code for a "file not
35
+ # found" error.
36
+ #
37
+ # Method from Rubocop::ConfigLoader
38
+ def read_file(absolute_path)
39
+ File.read(absolute_path, encoding: Encoding::UTF_8)
40
+ rescue Errno::ENOENT
41
+ raise ConfigNotFoundError, "Configuration file not found: #{absolute_path}"
42
+ end
43
+
44
+ # Method from Rubocop::ConfigLoader
45
+ def yaml_safe_load(yaml_code, filename)
46
+ yaml_safe_load!(yaml_code, filename)
47
+ rescue ::StandardError
48
+ if defined?(::SafeYAML)
49
+ raise 'SafeYAML is unmaintained, no longer needed and should be removed'
50
+ end
51
+
52
+ raise
53
+ end
54
+
55
+ # Method from Rubocop::ConfigLoader
56
+ def yaml_safe_load!(yaml_code, filename)
57
+ YAML.safe_load(
58
+ yaml_code, permitted_classes: [Regexp, Symbol], aliases: true, filename: filename, symbolize_names: true
59
+ )
60
+ end
61
+ end
62
+ end
63
+ end
@@ -0,0 +1,23 @@
1
+ # frozen_string_literal: true
2
+
3
+ module AnnotateRb
4
+ module Core
5
+ class << self
6
+ def version
7
+ @version ||= File.read(File.expand_path('../../VERSION', __dir__)).strip
8
+ end
9
+
10
+ def load_rake_tasks
11
+ return if @load_rake_tasks
12
+
13
+ rake_tasks = Dir[File.join(File.dirname(__FILE__), 'tasks', '**/*.rake')]
14
+
15
+ rake_tasks.each do |task|
16
+ load task
17
+ end
18
+
19
+ @load_rake_tasks = true
20
+ end
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,23 @@
1
+ # frozen_string_literal: true
2
+
3
+ module AnnotateRb
4
+ # Not sure what this does just yet
5
+ class EagerLoader
6
+ class << self
7
+ def call(options)
8
+ options[:require].count > 0 && options[:require].each { |path| require path }
9
+
10
+ if defined?(::Rails::Application)
11
+ klass = ::Rails::Application.send(:subclasses).first
12
+ klass.eager_load!
13
+ else
14
+ options[:model_dir].each do |dir|
15
+ ::Rake::FileList["#{dir}/**/*.rb"].each do |fname|
16
+ require File.expand_path(fname)
17
+ end
18
+ end
19
+ end
20
+ end
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,30 @@
1
+ # frozen_string_literal: true
2
+
3
+ module AnnotateRb
4
+ class Env
5
+ class << self
6
+ def read(key)
7
+ key = key.to_s unless key.is_a?(String)
8
+
9
+ ENV[key]
10
+ end
11
+
12
+ def write(key, value)
13
+ key = key.to_s unless key.is_a?(String)
14
+
15
+ ENV[key] = value.to_s
16
+ end
17
+
18
+ def fetch(key, default_value)
19
+ key = key.to_s unless key.is_a?(String)
20
+ val = read(key)
21
+
22
+ if val.nil?
23
+ default_value
24
+ else
25
+ val
26
+ end
27
+ end
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ module AnnotateRb
4
+ module ModelAnnotator
5
+ class AnnotationPatternGenerator
6
+ COMPAT_PREFIX = '== Schema Info'.freeze
7
+ COMPAT_PREFIX_MD = '## Schema Info'.freeze
8
+
9
+ class << self
10
+ def call(options = Options.from({}))
11
+ if options[:wrapper_open]
12
+ return /(?:^(\n|\r\n)?# (?:#{options[:wrapper_open]}).*(\n|\r\n)?# (?:#{COMPAT_PREFIX}|#{COMPAT_PREFIX_MD}).*?(\n|\r\n)(#.*(\n|\r\n))*(\n|\r\n)*)|^(\n|\r\n)?# (?:#{COMPAT_PREFIX}|#{COMPAT_PREFIX_MD}).*?(\n|\r\n)(#.*(\n|\r\n))*(\n|\r\n)*/
13
+ end
14
+ /^(\n|\r\n)?# (?:#{COMPAT_PREFIX}|#{COMPAT_PREFIX_MD}).*?(\n|\r\n)(#.*(\n|\r\n))*(\n|\r\n)*/
15
+ end
16
+ end
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,74 @@
1
+ # require 'bigdecimal'
2
+
3
+ module AnnotateRb
4
+ module ModelAnnotator
5
+ class Annotator
6
+ # Annotate Models plugin use this header
7
+ PREFIX = '== Schema Information'.freeze
8
+ PREFIX_MD = '## Schema Information'.freeze
9
+
10
+ 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
11
+
12
+ class << self
13
+ # We're passed a name of things that might be
14
+ # ActiveRecord models. If we can find the class, and
15
+ # if its a subclass of ActiveRecord::Base,
16
+ # then pass it to the associated block
17
+ def do_annotations(options = {})
18
+ header = options[:format_markdown] ? PREFIX_MD.dup : PREFIX.dup
19
+ version = ActiveRecord::Migrator.current_version rescue 0
20
+ if options[:include_version] && version > 0
21
+ header << "\n# Schema version: #{version}"
22
+ end
23
+
24
+ annotated = []
25
+ model_files_to_annotate = ModelFilesGetter.call(options)
26
+
27
+ model_files_to_annotate.each do |path, filename|
28
+ ModelFileAnnotator.call(annotated, File.join(path, filename), header, options)
29
+ end
30
+
31
+ if annotated.empty?
32
+ puts 'Model files unchanged.'
33
+ else
34
+ puts "Annotated (#{annotated.length}): #{annotated.join(', ')}"
35
+ end
36
+ end
37
+
38
+ def remove_annotations(options = {})
39
+ deannotated = []
40
+ deannotated_klass = false
41
+ ModelFilesGetter.call(options).each do |file|
42
+ file = File.join(file)
43
+ begin
44
+ klass = ModelClassGetter.call(file, options)
45
+ if klass < ActiveRecord::Base && !klass.abstract_class?
46
+ model_name = klass.name.underscore
47
+ table_name = klass.table_name
48
+ model_file_name = file
49
+ deannotated_klass = true if FileAnnotationRemover.call(model_file_name, options)
50
+
51
+ patterns = PatternGetter.call(options)
52
+
53
+ patterns
54
+ .map { |f| FileNameResolver.call(f, model_name, table_name) }
55
+ .each do |f|
56
+ if File.exist?(f)
57
+ FileAnnotationRemover.call(f, options)
58
+ deannotated_klass = true
59
+ end
60
+ end
61
+ end
62
+ deannotated << klass if deannotated_klass
63
+ rescue StandardError => e
64
+ $stderr.puts "Unable to deannotate #{File.join(file)}: #{e.message}"
65
+ $stderr.puts "\t" + e.backtrace.join("\n\t") if options[:trace]
66
+ end
67
+ end
68
+ puts "Removed annotations from: #{deannotated.join(', ')}"
69
+ end
70
+ end
71
+ end
72
+ end
73
+ end
74
+
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ module AnnotateRb
4
+ module ModelAnnotator
5
+ class BadModelFileError < LoadError
6
+ def to_s
7
+ "file doesn't contain a valid model class"
8
+ end
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,22 @@
1
+ module AnnotateRb
2
+ module ModelAnnotator
3
+ module Constants
4
+ TRUE_RE = /^(true|t|yes|y|1)$/i.freeze
5
+
6
+ ##
7
+ # The set of available options to customize the behavior of Annotate.
8
+ #
9
+ POSITION_OPTIONS = ::AnnotateRb::Options::POSITION_OPTION_KEYS
10
+
11
+ FLAG_OPTIONS = ::AnnotateRb::Options::FLAG_OPTION_KEYS
12
+
13
+ OTHER_OPTIONS = ::AnnotateRb::Options::OTHER_OPTION_KEYS
14
+
15
+ PATH_OPTIONS = ::AnnotateRb::Options::PATH_OPTION_KEYS
16
+
17
+ ALL_ANNOTATE_OPTIONS = ::AnnotateRb::Options::ALL_OPTION_KEYS
18
+
19
+ SKIP_ANNOTATION_PREFIX = '# -\*- SkipSchemaAnnotations'.freeze
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,25 @@
1
+ # frozen_string_literal: true
2
+
3
+ module AnnotateRb
4
+ module ModelAnnotator
5
+ class FileAnnotationRemover
6
+ class << self
7
+ def call(file_name, options = Options.from({}))
8
+ if File.exist?(file_name)
9
+ content = File.read(file_name)
10
+ return false if content =~ /#{Constants::SKIP_ANNOTATION_PREFIX}.*\n/
11
+
12
+ wrapper_open = options[:wrapper_open] ? "# #{options[:wrapper_open]}\n" : ''
13
+ content.sub!(/(#{wrapper_open})?#{AnnotationPatternGenerator.call(options)}/, '')
14
+
15
+ File.open(file_name, 'wb') { |f| f.puts content }
16
+
17
+ true
18
+ else
19
+ false
20
+ end
21
+ end
22
+ end
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,79 @@
1
+ # frozen_string_literal: true
2
+
3
+ module AnnotateRb
4
+ module ModelAnnotator
5
+ class FileAnnotator
6
+ class << self
7
+ # Add a schema block to a file. If the file already contains
8
+ # a schema info block (a comment starting with "== Schema Information"),
9
+ # check if it matches the block that is already there. If so, leave it be.
10
+ # If not, remove the old info block and write a new one.
11
+ #
12
+ # == Returns:
13
+ # true or false depending on whether the file was modified.
14
+ #
15
+ # === Options (opts)
16
+ # :force<Symbol>:: whether to update the file even if it doesn't seem to need it.
17
+ # :position_in_*<Symbol>:: where to place the annotated section in fixture or model file,
18
+ # :before, :top, :after or :bottom. Default is :before.
19
+ #
20
+ def call(file_name, info_block, position, options = {})
21
+ return false unless File.exist?(file_name)
22
+ old_content = File.read(file_name)
23
+ return false if old_content =~ /#{Constants::SKIP_ANNOTATION_PREFIX}.*\n/
24
+
25
+ # Ignore the Schema version line because it changes with each migration
26
+ header_pattern = /(^# Table name:.*?\n(#.*[\r]?\n)*[\r]?)/
27
+ old_header = old_content.match(header_pattern).to_s
28
+ new_header = info_block.match(header_pattern).to_s
29
+
30
+ column_pattern = /^#[\t ]+[\w\*\.`]+[\t ]+.+$/
31
+ old_columns = old_header && old_header.scan(column_pattern).sort
32
+ new_columns = new_header && new_header.scan(column_pattern).sort
33
+
34
+ return false if old_columns == new_columns && !options[:force]
35
+
36
+ abort "annotate error. #{file_name} needs to be updated, but annotate was run with `--frozen`." if options[:frozen]
37
+
38
+ # Replace inline the old schema info with the new schema info
39
+ wrapper_open = options[:wrapper_open] ? "# #{options[:wrapper_open]}\n" : ""
40
+ wrapper_close = options[:wrapper_close] ? "# #{options[:wrapper_close]}\n" : ""
41
+ wrapped_info_block = "#{wrapper_open}#{info_block}#{wrapper_close}"
42
+
43
+ annotation_pattern = AnnotationPatternGenerator.call(options)
44
+ old_annotation = old_content.match(annotation_pattern).to_s
45
+
46
+ # if there *was* no old schema info or :force was passed, we simply
47
+ # need to insert it in correct position
48
+ if old_annotation.empty? || options[:force]
49
+ magic_comments_block = Helper.magic_comments_as_string(old_content)
50
+ old_content.gsub!(Annotator::MAGIC_COMMENT_MATCHER, '')
51
+
52
+ annotation_pattern = AnnotationPatternGenerator.call(options)
53
+ old_content.sub!(annotation_pattern, '')
54
+
55
+ new_content = if %w(after bottom).include?(options[position].to_s)
56
+ magic_comments_block + (old_content.rstrip + "\n\n" + wrapped_info_block)
57
+ elsif magic_comments_block.empty?
58
+ magic_comments_block + wrapped_info_block + old_content.lstrip
59
+ else
60
+ magic_comments_block + "\n" + wrapped_info_block + old_content.lstrip
61
+ end
62
+ else
63
+ # replace the old annotation with the new one
64
+
65
+ # keep the surrounding whitespace the same
66
+ space_match = old_annotation.match(/\A(?<start>\s*).*?\n(?<end>\s*)\z/m)
67
+ new_annotation = space_match[:start] + wrapped_info_block + space_match[:end]
68
+
69
+ annotation_pattern = AnnotationPatternGenerator.call(options)
70
+ new_content = old_content.sub(annotation_pattern, new_annotation)
71
+ end
72
+
73
+ File.open(file_name, 'wb') { |f| f.puts new_content }
74
+ true
75
+ end
76
+ end
77
+ end
78
+ end
79
+ end