annotaterb 4.0.0.beta.1

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