awesome_annotate 0.1.5 → 0.3.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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: add6a26de84b0dab25e21c0c60213a8d0c6aef0109a552faf635fdfb745e5bcb
4
- data.tar.gz: 6a9494f2aeafe2e163c59f1544f181da0d39aef6736851412580c1b59231631a
3
+ metadata.gz: 4c4c2fe792fc935930fcc07c2a9662f7032cd590bcad80f06551e5dae567f279
4
+ data.tar.gz: 62db756fb9535984c36eda116f23aa2130ba18b2077c8102423d8b559e075e78
5
5
  SHA512:
6
- metadata.gz: 9654317d619c0d5e4706b1a5fb1f6899a123a3f8b6acb79c29d744d65ed5c6d1a895e9cbd835653e1d33de80055c75a24e0f38fd365d2e58112738cf2e70b763
7
- data.tar.gz: 807524f834b4087643d8fc2e2477ebd271f6eac9a989d947b9ac3dd693259baf7890187e3201293d4e809f6dfef9f7abe05542f3a287e17af0a527d54d955acc
6
+ metadata.gz: 33795981d662f1eeae3be75bb11befd25aaed09d22f50c26647d131c1916ea54eb9f2a105272a8e42e4cb6c80a5df3c474cda12c4a9c566c0e9452512d40ac33
7
+ data.tar.gz: ae8e56afa0844d88bbb2d1cae87e12538846ccf507394943259c8856e548d7e70e9c1e49cdc8d42ce3c7e8ada99900df46f8b251396cdb8161f976c4ad25ae2f
data/.rubocop.yml CHANGED
@@ -1,18 +1,50 @@
1
- inherit_from:
2
- - .rubocop_todo.yml
3
-
4
1
  require:
5
2
  - rubocop-rake
6
3
  - rubocop-rspec
7
4
 
8
5
  AllCops:
6
+ TargetRubyVersion: 3.0
9
7
  Exclude:
10
8
  - 'vendor/**/*'
11
9
  - 'spec/fixtures/**/*'
12
10
  - 'tmp/**/*'
13
- - 'spec/integration/**/*'
14
11
  NewCops: enable
15
12
 
13
+ Style/Documentation:
14
+ Enabled: false
15
+
16
16
  Metrics/BlockLength:
17
17
  Exclude:
18
18
  - 'spec/**/*.rb'
19
+
20
+ Metrics/AbcSize:
21
+ Exclude:
22
+ - 'spec/support/**/*'
23
+
24
+ RSpec/VerifiedDoubles:
25
+ Exclude:
26
+ - 'spec/support/**/*'
27
+
28
+ RSpec/MultipleExpectations:
29
+ Exclude:
30
+ - 'spec/**/*'
31
+
32
+ RSpec/NestedGroups:
33
+ Exclude:
34
+ - 'spec/**/*'
35
+
36
+ RSpec/HooksBeforeExamples:
37
+ Exclude:
38
+ - 'spec/**/*'
39
+
40
+ RSpec/ExampleLength:
41
+ Exclude:
42
+ - 'spec/**/*'
43
+
44
+ RSpec/DescribeClass:
45
+ Exclude:
46
+ - 'spec/integration/**/*'
47
+
48
+ Lint/EmptyClass:
49
+ Exclude:
50
+ - 'spec/support/rails.rb'
data/.ruby-version ADDED
@@ -0,0 +1 @@
1
+ 3.2.2
data/CHANGELOG.md CHANGED
@@ -1,5 +1,53 @@
1
1
  # Changelog
2
2
 
3
+ ## [0.3.1] - 2026-05-31
4
+
5
+ - Fixed a bug
6
+
7
+ ## [0.1.0] - 2024-04-29
8
+
9
+ - Initial release
10
+
11
+ ## [0.3.0] - 2026-05-31
12
+
13
+ - Added `awesome_annotate init` to create
14
+ `config/initializers/awesome_annotate.yml`.
15
+ - Added configuration loading for annotation and remove commands.
16
+ - Added path configuration:
17
+ - `env_file_path`
18
+ - `model_dir`
19
+ - `route_file_path`
20
+ - Added `annotation_position` to choose whether new annotation blocks are
21
+ inserted at the top or bottom of files.
22
+ - Added model annotation filters and output controls:
23
+ - `exclude_model_files`
24
+ - `include_indexes`
25
+ - `exclude_columns`
26
+ - `include_column_defaults`
27
+ - Added `exclude_routes` to omit matching route lines from route annotations.
28
+ - Added validation for supported configuration value types.
29
+
30
+ ## [0.2.0] - 2026-05-10
31
+
32
+ - Added model schema annotations with column type, nullability, primary key,
33
+ default value, and basic index information.
34
+ - Added duplicate-safe annotation blocks with AwesomeAnnotate start and end
35
+ markers.
36
+ - Added route annotations for `config/routes.rb`.
37
+ - Added model commands:
38
+ - `awesome_annotate model MODEL`
39
+ - `awesome_annotate models`
40
+ - `awesome_annotate models MODEL...`
41
+ - Added `awesome_annotate all` to annotate all models and routes.
42
+ - Added remove commands:
43
+ - `awesome_annotate remove model MODEL`
44
+ - `awesome_annotate remove models`
45
+ - `awesome_annotate remove routes`
46
+ - `awesome_annotate remove all`
47
+ - Added Rails-like integration specs and GitHub Actions CI for RSpec and
48
+ RuboCop.
49
+ - Documented supported Ruby and Active Record versions.
50
+
3
51
  ## [0.1.3] - 2024-05-05
4
52
 
5
53
  - Add a new feature
data/README.md CHANGED
@@ -1,35 +1,245 @@
1
1
  # AwesomeAnnotate
2
2
 
3
- TODO: Delete this and the text below, and describe your gem
3
+ AwesomeAnnotate adds generated schema and route comments to Rails applications.
4
4
 
5
- Welcome to your new gem! In this directory, you'll find the files you need to be able to package up your Ruby library into a gem. Put your Ruby code in the file `lib/awesome_annotate`. To experiment with that code, run `bin/console` for an interactive prompt.
5
+ This gem is intended as a small replacement-style tool for the basic features of
6
+ the original `annotate` gem:
7
+
8
+ - annotate model files with Active Record schema information
9
+ - annotate `config/routes.rb` with the application's route table
10
+ - replace previous AwesomeAnnotate blocks instead of appending duplicates
11
+ - remove generated AwesomeAnnotate blocks
12
+
13
+ ## Requirements
14
+
15
+ - Ruby 3.0 or later, below 4.0
16
+ - Active Record 6.1 or later, below 8.0
17
+ - Rails application layout with `config/environment.rb`
18
+
19
+ Development currently uses the Ruby version in `.ruby-version`.
6
20
 
7
21
  ## Installation
8
22
 
9
- TODO: Replace `UPDATE_WITH_YOUR_GEM_NAME_IMMEDIATELY_AFTER_RELEASE_TO_RUBYGEMS_ORG` with your gem name right after releasing it to RubyGems.org. Please do not do it earlier due to security reasons. Alternatively, replace this section with instructions to install your gem from git if you don't plan to release to RubyGems.org.
23
+ Add this line to your Rails application's Gemfile:
24
+
25
+ ```ruby
26
+ gem 'awesome_annotate'
27
+ ```
10
28
 
11
- Install the gem and add to the application's Gemfile by executing:
29
+ Then run:
12
30
 
13
- $ bundle add UPDATE_WITH_YOUR_GEM_NAME_IMMEDIATELY_AFTER_RELEASE_TO_RUBYGEMS_ORG
31
+ ```sh
32
+ bundle install
33
+ ```
14
34
 
15
- If bundler is not being used to manage dependencies, install the gem by executing:
35
+ If you are using this repository directly before publishing the gem, use a Git
36
+ source instead:
16
37
 
17
- $ gem install UPDATE_WITH_YOUR_GEM_NAME_IMMEDIATELY_AFTER_RELEASE_TO_RUBYGEMS_ORG
38
+ ```ruby
39
+ gem 'awesome_annotate', git: 'https://github.com/wisdom-plus/awesome_annotate'
40
+ ```
18
41
 
19
42
  ## Usage
20
43
 
21
- TODO: Write usage instructions here
44
+ Run commands from the root directory of a Rails application.
45
+
46
+ Available commands:
47
+
48
+ ```sh
49
+ bundle exec awesome_annotate model user
50
+ bundle exec awesome_annotate models
51
+ bundle exec awesome_annotate models user article admin/user
52
+ bundle exec awesome_annotate routes
53
+ bundle exec awesome_annotate all
54
+ bundle exec awesome_annotate init
55
+ bundle exec awesome_annotate remove model user
56
+ bundle exec awesome_annotate remove models
57
+ bundle exec awesome_annotate remove routes
58
+ bundle exec awesome_annotate remove all
59
+ ```
60
+
61
+ Annotate a model:
62
+
63
+ ```sh
64
+ bundle exec awesome_annotate model user
65
+ ```
66
+
67
+ Annotate all models:
68
+
69
+ ```sh
70
+ bundle exec awesome_annotate models
71
+ ```
72
+
73
+ Annotate specific models:
74
+
75
+ ```sh
76
+ bundle exec awesome_annotate models user article admin/user
77
+ ```
78
+
79
+ Annotate all models and routes:
80
+
81
+ ```sh
82
+ bundle exec awesome_annotate all
83
+ ```
84
+
85
+ Remove generated annotations:
86
+
87
+ ```sh
88
+ bundle exec awesome_annotate remove model user
89
+ bundle exec awesome_annotate remove models
90
+ bundle exec awesome_annotate remove routes
91
+ bundle exec awesome_annotate remove all
92
+ ```
93
+
94
+ Create a configuration file:
95
+
96
+ ```sh
97
+ bundle exec awesome_annotate init
98
+ ```
99
+
100
+ This creates `config/initializers/awesome_annotate.yml`:
101
+
102
+ ```yaml
103
+ # AwesomeAnnotate configuration
104
+ #
105
+ # Change these paths when your Rails app uses non-standard locations.
106
+ env_file_path: config/environment.rb
107
+ model_dir: app/models
108
+ route_file_path: config/routes.rb
109
+ annotation_position: top
110
+ exclude_model_files: []
111
+ include_indexes: true
112
+ exclude_columns: []
113
+ include_column_defaults: true
114
+ exclude_routes: []
115
+ ```
116
+
117
+ When this file exists, annotation and removal commands use these values. This
118
+ allows applications with non-standard model, route, or environment file paths to
119
+ change command behavior without passing Ruby options directly. Set
120
+ `annotation_position` to `top` or `bottom` to choose where new annotation blocks
121
+ are inserted. Set `exclude_model_files` to relative file paths or glob patterns
122
+ under `model_dir` to skip them during model discovery. Set `include_indexes` to
123
+ `false` to omit the Indexes section from model schema annotations. Set
124
+ `exclude_columns` to column names to omit them from model schema annotations. Set
125
+ `include_column_defaults` to `false` to omit `default(...)` details. Set
126
+ `exclude_routes` to route line patterns to omit them from route annotations.
127
+
128
+ This loads `config/environment.rb`, resolves `User`, reads its Active Record
129
+ columns, and writes a schema block before the class definition in
130
+ `app/models/user.rb`:
131
+
132
+ ```ruby
133
+ # == AwesomeAnnotate: columns
134
+ # == Schema Information
135
+ #
136
+ # Table name: users
137
+ #
138
+ # id :integer not null, primary key
139
+ # name :string
140
+ # email :string not null, default("")
141
+ # created_at :datetime not null
142
+ # updated_at :datetime not null
143
+ #
144
+ # Indexes
145
+ #
146
+ # (email) UNIQUE, index_users_on_email
147
+ # (name,email) index_users_on_name_and_email
148
+ #
149
+ # == /AwesomeAnnotate: columns
150
+ class User < ApplicationRecord
151
+ end
152
+ ```
153
+
154
+ Annotate routes:
155
+
156
+ ```sh
157
+ bundle exec awesome_annotate routes
158
+ ```
159
+
160
+ This writes a generated route block before `Rails.application.routes.draw do` in
161
+ `config/routes.rb`:
162
+
163
+ ```ruby
164
+ # == AwesomeAnnotate: routes
165
+ # Prefix Verb URI Pattern Controller#Action
166
+ # users GET /users(.:format) users#index
167
+ # == /AwesomeAnnotate: routes
168
+ Rails.application.routes.draw do
169
+ end
170
+ ```
171
+
172
+ Print the gem version:
173
+
174
+ ```sh
175
+ bundle exec awesome_annotate --version
176
+ ```
177
+
178
+ Short aliases are also available:
179
+
180
+ ```sh
181
+ bundle exec awesome_annotate -m user
182
+ bundle exec awesome_annotate -r
183
+ bundle exec awesome_annotate -v
184
+ ```
185
+
186
+ ## Generated Blocks
187
+
188
+ AwesomeAnnotate wraps generated comments with start and end markers:
189
+
190
+ ```ruby
191
+ # == AwesomeAnnotate: columns
192
+ # ...
193
+ # == /AwesomeAnnotate: columns
194
+ ```
195
+
196
+ When the command is run again, the existing block with the same marker is
197
+ replaced. This prevents duplicate annotations from being appended on repeated
198
+ runs.
199
+
200
+ ## Current Limitations
201
+
202
+ - Only model schema blocks and routes are supported.
203
+ - Model annotations include column type, nullability, primary key, and default
204
+ values, plus basic index information.
205
+ - Model annotation currently targets files under `app/models`.
206
+ - Model class detection expects a simple class declaration such as
207
+ `class User < ApplicationRecord`.
208
+ - Existing comments generated by other annotate tools are not migrated or
209
+ removed automatically.
210
+ - Ruby 4 is not currently supported.
22
211
 
23
212
  ## Development
24
213
 
25
- After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake spec` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
214
+ Install dependencies:
215
+
216
+ ```sh
217
+ bundle install
218
+ ```
219
+
220
+ Run the test suite:
221
+
222
+ ```sh
223
+ bundle exec rspec
224
+ ```
225
+
226
+ Run RuboCop:
227
+
228
+ ```sh
229
+ bundle exec rubocop
230
+ ```
231
+
232
+ Build the gem locally:
26
233
 
27
- To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and the created tag, and push the `.gem` file to [rubygems.org](https://rubygems.org).
234
+ ```sh
235
+ bundle exec rake build
236
+ ```
28
237
 
29
238
  ## Contributing
30
239
 
31
- Bug reports and pull requests are welcome on GitHub at https://github.com/[USERNAME]/awesome_annotate.
240
+ Bug reports and pull requests are welcome on GitHub at
241
+ https://github.com/wisdom-plus/awesome_annotate.
32
242
 
33
243
  ## License
34
244
 
35
- The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
245
+ The gem is available as open source under the terms of the MIT License.
data/Rakefile CHANGED
@@ -1,11 +1,11 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "bundler/gem_tasks"
4
- require "rspec/core/rake_task"
3
+ require 'bundler/gem_tasks'
4
+ require 'rspec/core/rake_task'
5
5
 
6
6
  RSpec::Core::RakeTask.new(:spec)
7
7
 
8
- require "rubocop/rake_task"
8
+ require 'rubocop/rake_task'
9
9
 
10
10
  RuboCop::RakeTask.new
11
11
 
data/exe/awesome_annotate CHANGED
@@ -1,8 +1,7 @@
1
1
  #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
2
3
 
3
- unless File.exist?('./Rakefile') || File.exist?('./Gemfile')
4
- abort 'Please run annotate from the root of the project.'
5
- end
4
+ abort 'Please run annotate from the root of the project.' unless File.exist?('./Rakefile') || File.exist?('./Gemfile')
6
5
  require 'awesome_annotate'
7
6
 
8
7
  AwesomeAnnotate::CLI.start ARGV
@@ -0,0 +1,49 @@
1
+ # frozen_string_literal: true
2
+
3
+ module AwesomeAnnotate
4
+ module AnnotationBlock
5
+ private
6
+
7
+ def replace_or_insert_annotation(file_path:, marker:, content:, before:, position: 'top')
8
+ path = file_path.to_s
9
+ file_content = File.read(path)
10
+ annotation = annotation_block(marker, content)
11
+
12
+ File.write(path, replace_annotation(file_content, marker, annotation, before, position))
13
+ end
14
+
15
+ def remove_annotation(file_path:, marker:)
16
+ path = file_path.to_s
17
+ file_content = File.read(path)
18
+ pattern = annotation_block_pattern(marker)
19
+
20
+ return false unless file_content.match?(pattern)
21
+
22
+ File.write(path, file_content.gsub(pattern, ''))
23
+ true
24
+ end
25
+
26
+ def annotation_block(marker, content)
27
+ body = content.end_with?("\n") ? content : "#{content}\n"
28
+
29
+ "# == AwesomeAnnotate: #{marker}\n" \
30
+ "#{body}" \
31
+ "# == /AwesomeAnnotate: #{marker}\n\n"
32
+ end
33
+
34
+ def annotation_block_pattern(marker)
35
+ escaped_marker = Regexp.escape(marker)
36
+ %r{^# == AwesomeAnnotate: #{escaped_marker}\n.*?^# == /AwesomeAnnotate: #{escaped_marker}\n(?:\n)*}m
37
+ end
38
+
39
+ def replace_annotation(file_content, marker, annotation, before, position)
40
+ pattern = annotation_block_pattern(marker)
41
+
42
+ return file_content.sub(pattern, annotation) if file_content.match?(pattern)
43
+
44
+ return "#{file_content.chomp}\n#{annotation}" if position.to_s == 'bottom'
45
+
46
+ file_content.sub(before, "#{annotation}\\0")
47
+ end
48
+ end
49
+ end
@@ -1,15 +1,55 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'awesome_annotate'
4
+ require 'awesome_annotate/configuration'
2
5
  require 'awesome_annotate/model'
3
6
  require 'awesome_annotate/route'
4
7
  require 'awesome_annotate/version'
5
8
  require 'thor'
6
9
 
7
10
  module AwesomeAnnotate
11
+ class Remove < Thor
12
+ desc 'model [model_name]', 'remove annotation from a model'
13
+ def model(model_name)
14
+ model_annotator.remove(model_name)
15
+ end
16
+
17
+ desc 'models [model_names...]', 'remove annotations from all models or specified models'
18
+ def models(*model_names)
19
+ model_annotator.remove_all(model_names)
20
+ end
21
+
22
+ desc 'routes', 'remove annotation from `config/routes.rb`'
23
+ def routes
24
+ route_annotator.remove
25
+ end
26
+
27
+ desc 'all', 'remove annotations from all models and routes'
28
+ def all
29
+ model_annotator.remove_all
30
+ route_annotator.remove
31
+ end
32
+
33
+ private
34
+
35
+ def model_annotator
36
+ AwesomeAnnotate::Model.new(configuration.options)
37
+ end
38
+
39
+ def route_annotator
40
+ AwesomeAnnotate::Route.new(configuration.options)
41
+ end
42
+
43
+ def configuration
44
+ AwesomeAnnotate::Configuration.load
45
+ end
46
+ end
47
+
8
48
  class CLI < Thor
9
49
  include Thor::Actions
10
50
 
11
51
  map %w[--version -v] => :print_version
12
- desc "--version, -v", "print the version"
52
+ desc '--version, -v', 'print the version'
13
53
  def print_version
14
54
  say AwesomeAnnotate::VERSION
15
55
  end
@@ -17,19 +57,56 @@ module AwesomeAnnotate
17
57
  map %w[model -m] => :model
18
58
  desc 'model [model_name]', 'annotate your model'
19
59
  def model(model_name)
20
- AwesomeAnnotate::Model.new.annotate(model_name)
60
+ model_annotator.annotate(model_name)
61
+ end
62
+
63
+ desc 'models [model_names...]', 'annotate all models or specified models'
64
+ def models(*model_names)
65
+ model_annotator.annotate_all(model_names)
21
66
  end
22
67
 
23
68
  map %w[routes -r] => :routes
24
- desc 'routes', "Writes application route information to `config/routes.rb`."
69
+ desc 'routes', 'Writes application route information to `config/routes.rb`.'
25
70
  def routes
26
- AwesomeAnnotate::Route.new.annotate
71
+ route_annotator.annotate
27
72
  end
28
73
 
29
- private
74
+ desc 'all', 'annotate all models and routes'
75
+ def all
76
+ model_annotator.annotate_all
77
+ route_annotator.annotate
78
+ end
79
+
80
+ desc 'init', "create #{AwesomeAnnotate::Configuration::DEFAULT_PATH}"
81
+ def init
82
+ path = AwesomeAnnotate::Configuration::DEFAULT_PATH
83
+ if File.exist?(path)
84
+ say "Config file already exists: #{path}"
85
+ else
86
+ AwesomeAnnotate::Configuration.create(path)
87
+ say "create #{path}"
88
+ end
89
+ end
90
+
91
+ desc 'remove SUBCOMMAND', 'remove generated annotations'
92
+ subcommand 'remove', Remove
30
93
 
31
94
  def self.exit_on_failure?
32
95
  true
33
96
  end
97
+
98
+ private
99
+
100
+ def model_annotator
101
+ AwesomeAnnotate::Model.new(configuration.options)
102
+ end
103
+
104
+ def route_annotator
105
+ AwesomeAnnotate::Route.new(configuration.options)
106
+ end
107
+
108
+ def configuration
109
+ AwesomeAnnotate::Configuration.load
110
+ end
34
111
  end
35
112
  end
@@ -0,0 +1,112 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'fileutils'
4
+ require 'yaml'
5
+
6
+ module AwesomeAnnotate
7
+ class Configuration
8
+ DEFAULT_PATH = 'config/initializers/awesome_annotate.yml'
9
+ DEFAULT_OPTIONS = {
10
+ env_file_path: 'config/environment.rb',
11
+ model_dir: 'app/models',
12
+ route_file_path: 'config/routes.rb',
13
+ annotation_position: 'top',
14
+ exclude_model_files: [],
15
+ include_indexes: true,
16
+ exclude_columns: [],
17
+ include_column_defaults: true,
18
+ exclude_routes: []
19
+ }.freeze
20
+ ANNOTATION_POSITIONS = %w[top bottom].freeze
21
+
22
+ TEMPLATE = <<~YAML
23
+ # AwesomeAnnotate configuration
24
+ #
25
+ # Change these paths when your Rails app uses non-standard locations.
26
+ env_file_path: config/environment.rb
27
+ model_dir: app/models
28
+ route_file_path: config/routes.rb
29
+ annotation_position: top
30
+ exclude_model_files: []
31
+ include_indexes: true
32
+ exclude_columns: []
33
+ include_column_defaults: true
34
+ exclude_routes: []
35
+ YAML
36
+
37
+ class << self
38
+ def load(path = DEFAULT_PATH)
39
+ return new unless File.exist?(path)
40
+
41
+ loaded = YAML.safe_load_file(path, aliases: false) || {}
42
+ raise ArgumentError, "Configuration file must contain a YAML mapping: #{path}" unless loaded.is_a?(Hash)
43
+
44
+ new(symbolize_options(loaded))
45
+ end
46
+
47
+ def create(path = DEFAULT_PATH)
48
+ FileUtils.mkdir_p(File.dirname(path))
49
+ File.write(path, TEMPLATE)
50
+ end
51
+
52
+ private
53
+
54
+ def symbolize_options(options)
55
+ options.each_with_object({}) do |(key, value), result|
56
+ symbol_key = key.to_sym
57
+ result[symbol_key] = value if DEFAULT_OPTIONS.key?(symbol_key)
58
+ end
59
+ end
60
+ end
61
+
62
+ attr_reader :options
63
+
64
+ def initialize(options = {})
65
+ @options = DEFAULT_OPTIONS.merge(options)
66
+ validate_annotation_position
67
+ validate_exclude_model_files
68
+ validate_include_indexes
69
+ validate_exclude_columns
70
+ validate_include_column_defaults
71
+ validate_exclude_routes
72
+ end
73
+
74
+ private
75
+
76
+ def validate_annotation_position
77
+ return if ANNOTATION_POSITIONS.include?(@options[:annotation_position])
78
+
79
+ raise ArgumentError, "annotation_position must be one of: #{ANNOTATION_POSITIONS.join(', ')}"
80
+ end
81
+
82
+ def validate_exclude_model_files
83
+ return if @options[:exclude_model_files].is_a?(Array)
84
+
85
+ raise ArgumentError, 'exclude_model_files must be an array'
86
+ end
87
+
88
+ def validate_include_indexes
89
+ return if [true, false].include?(@options[:include_indexes])
90
+
91
+ raise ArgumentError, 'include_indexes must be true or false'
92
+ end
93
+
94
+ def validate_exclude_columns
95
+ return if @options[:exclude_columns].is_a?(Array)
96
+
97
+ raise ArgumentError, 'exclude_columns must be an array'
98
+ end
99
+
100
+ def validate_include_column_defaults
101
+ return if [true, false].include?(@options[:include_column_defaults])
102
+
103
+ raise ArgumentError, 'include_column_defaults must be true or false'
104
+ end
105
+
106
+ def validate_exclude_routes
107
+ return if @options[:exclude_routes].is_a?(Array)
108
+
109
+ raise ArgumentError, 'exclude_routes must be an array'
110
+ end
111
+ end
112
+ end
@@ -1,2 +1,6 @@
1
+ # frozen_string_literal: true
1
2
 
2
- class NotFoundError < StandardError; end
3
+ module AwesomeAnnotate
4
+ class Error < StandardError; end
5
+ class NotFoundError < Error; end
6
+ end
@@ -1,73 +1,142 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'active_record'
2
4
  require 'thor'
5
+ require_relative 'annotation_block'
3
6
  require_relative 'error'
7
+ require_relative 'rails_environment'
8
+ require_relative 'schema_annotation'
4
9
 
5
10
  module AwesomeAnnotate
6
11
  class Model < Thor
12
+ include AnnotationBlock
7
13
  include Thor::Actions
14
+ include RailsEnvironment
15
+ include SchemaAnnotation
8
16
 
9
17
  def initialize(params = {})
10
18
  super()
11
19
  @env_file_path = Pathname.new(params[:env_file_path] || 'config/environment.rb')
12
20
  @model_dir = Pathname.new(params[:model_dir] || 'app/models')
21
+ @annotation_position = params[:annotation_position] || 'top'
22
+ @exclude_model_files = params[:exclude_model_files] || []
23
+ @include_indexes = params.fetch(:include_indexes, true)
24
+ @exclude_columns = params[:exclude_columns] || []
25
+ @include_column_defaults = params.fetch(:include_column_defaults, true)
13
26
  end
14
27
 
15
28
  desc 'model [model name]', 'annotate your model'
16
29
  def annotate(model_name)
17
- raise "Rails application path is required" unless @env_file_path.exist?
30
+ raise 'Rails application path is required' unless @env_file_path.exist?
31
+
32
+ load_rails_environment
33
+ annotate_loaded_model(model_name)
34
+ end
35
+
36
+ desc 'models [model names]', 'annotate all models or specified models'
37
+ def annotate_all(model_names = [])
38
+ raise 'Rails application path is required' unless @env_file_path.exist?
18
39
 
19
- apply @env_file_path.to_s
40
+ load_rails_environment
20
41
 
21
- klass = klass_name(model_name)
42
+ if model_names.empty?
43
+ discover_model_names.each { |model_name| annotate_discovered_model(model_name) }
44
+ else
45
+ model_names.each { |model_name| annotate_loaded_model(model_name) }
46
+ end
47
+ end
48
+
49
+ desc 'remove [model name]', 'remove annotation from your model'
50
+ def remove(model_name)
51
+ file_path = model_file_path(model_name)
52
+
53
+ remove_model_annotation(file_path)
54
+ end
55
+
56
+ desc 'remove_all [model names]', 'remove annotations from all models or specified models'
57
+ def remove_all(model_names = [])
58
+ if model_names.empty?
59
+ discovered_model_file_paths.each { |file_path| remove_model_annotation(file_path, report_missing: false) }
60
+ else
61
+ model_names.each { |model_name| remove(model_name) }
62
+ end
63
+ end
64
+
65
+ private
66
+
67
+ def annotate_loaded_model(model_name, report_missing: true)
68
+ klass = klass_name(model_name, report_missing: report_missing)
22
69
 
23
70
  return say 'This model does not inherit activerecord' unless klass < ActiveRecord::Base
24
71
 
25
- column_names = column_names(klass)
26
72
  file_path = model_file_path(model_name)
27
73
 
28
- insert_file_before_class(file_path, klass, "# Columns: #{column_names.join(', ')}\n")
74
+ insert_file_before_class(file_path, schema_annotation(klass))
29
75
 
30
76
  say "annotate #{model_name.pluralize} table columns in #{file_path}"
31
77
  end
32
78
 
33
- private
79
+ def discover_model_names
80
+ discovered_model_file_paths.map { |file_path| model_name_from_file_path(file_path) }
81
+ end
82
+
83
+ def discovered_model_file_paths = Dir.glob(@model_dir.join('**/*.rb')).reject { |path| excluded_model_file?(path) }
84
+
85
+ def model_name_from_file_path(file_path)
86
+ Pathname.new(file_path).relative_path_from(@model_dir).sub_ext('').to_s
87
+ end
88
+
89
+ def excluded_model_file?(file_path)
90
+ relative_path = Pathname.new(file_path).relative_path_from(@model_dir).to_s
34
91
 
35
- def model_dir
36
- Pathname.new('app/models')
92
+ relative_path == 'application_record.rb' || relative_path.start_with?('concerns/') ||
93
+ excluded_by_config?(relative_path)
37
94
  end
38
95
 
39
- def insert_file_before_class(file_path, klass, message)
40
- insert_into_file file_path, :before => /^class\s+\w+\s+<\s+\w+/ do
41
- message
96
+ def excluded_by_config?(relative_path)
97
+ @exclude_model_files.any? { |pattern| File.fnmatch?(pattern, relative_path, File::FNM_PATHNAME) }
98
+ end
99
+
100
+ def annotate_discovered_model(model_name)
101
+ annotate_loaded_model(model_name, report_missing: false)
102
+ rescue AwesomeAnnotate::NotFoundError
103
+ nil
104
+ end
105
+
106
+ def remove_model_annotation(file_path, report_missing: true)
107
+ if remove_annotation(file_path: file_path, marker: 'columns')
108
+ say "remove model annotation in #{file_path}"
109
+ elsif report_missing
110
+ say "no model annotation in #{file_path}"
42
111
  end
43
112
  end
44
113
 
45
- def column_names(klass)
46
- klass.column_names
114
+ def insert_file_before_class(file_path, message)
115
+ replace_or_insert_annotation(file_path: file_path, marker: 'columns', content: message,
116
+ before: /^class\s+\w+\s+<\s+\w+/, position: @annotation_position)
47
117
  end
48
118
 
49
119
  def model_file_path(model_name)
50
120
  file_path = "#{@model_dir}/#{model_name}.rb"
51
121
 
52
122
  unless File.exist?(file_path)
53
- say "Model file not found"
54
- raise NotFoundError
123
+ say 'Model file not found'
124
+ raise AwesomeAnnotate::NotFoundError
55
125
  end
56
126
 
57
- return file_path
127
+ file_path
58
128
  end
59
129
 
60
- def klass_name(model_name)
130
+ def klass_name(model_name, report_missing: true)
61
131
  name = model_name.singularize.camelize
62
- return Object.const_get(name)
63
-
132
+ Object.const_get(name)
64
133
  rescue NameError
65
- say "Model not found"
66
- raise NotFoundError
134
+ say 'Model not found' if report_missing
135
+ raise AwesomeAnnotate::NotFoundError
67
136
  end
68
137
 
69
- def self.source_root
70
- Dir.pwd
71
- end
138
+ def self.source_root = Dir.pwd
139
+
140
+ private_class_method :source_root
72
141
  end
73
142
  end
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ module AwesomeAnnotate
4
+ module RailsEnvironment
5
+ private
6
+
7
+ def load_rails_environment
8
+ require @env_file_path.expand_path.to_s
9
+ end
10
+ end
11
+ end
@@ -1,21 +1,29 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'active_record'
2
4
  require 'thor'
5
+ require_relative 'annotation_block'
6
+ require_relative 'rails_environment'
3
7
 
4
8
  module AwesomeAnnotate
5
9
  class Route < Thor
10
+ include AnnotationBlock
6
11
  include Thor::Actions
12
+ include RailsEnvironment
7
13
 
8
14
  def initialize(params = {})
9
15
  super()
10
16
  @env_file_path = Pathname.new(params[:env_file_path] || 'config/environment.rb')
11
17
  @route_file_path = Pathname.new(params[:route_file_path] || 'config/routes.rb')
18
+ @annotation_position = params[:annotation_position] || 'top'
19
+ @exclude_routes = params[:exclude_routes] || []
12
20
  end
13
21
 
14
22
  desc 'annotate all routes', 'annotate your routes'
15
23
  def annotate
16
- raise "Rails application path is required" unless @env_file_path.exist?
24
+ raise 'Rails application path is required' unless @env_file_path.exist?
17
25
 
18
- apply @env_file_path.to_s
26
+ load_rails_environment
19
27
 
20
28
  inspector = ActionDispatch::Routing::RoutesInspector.new(Rails.application.routes.routes)
21
29
  formatter = ActionDispatch::Routing::ConsoleFormatter::Sheet.new
@@ -23,17 +31,28 @@ module AwesomeAnnotate
23
31
  routes = inspector.format(formatter, {})
24
32
  route_message = parse_routes(routes)
25
33
 
26
- raise "Route file not found" unless @route_file_path.exist?
34
+ raise 'Route file not found' unless @route_file_path.exist?
27
35
 
28
36
  insert_file_before_class(@route_file_path, route_message)
29
37
 
30
38
  say "annotate routes in #{@route_file_path}"
31
39
  end
32
40
 
41
+ desc 'remove', 'remove route annotation'
42
+ def remove
43
+ raise 'Route file not found' unless @route_file_path.exist?
44
+
45
+ if remove_annotation(file_path: @route_file_path, marker: 'routes')
46
+ say "remove route annotation in #{@route_file_path}"
47
+ else
48
+ say "no route annotation in #{@route_file_path}"
49
+ end
50
+ end
51
+
33
52
  private
34
53
 
35
54
  def parse_routes(routes)
36
- split_routes = routes.split(/\r\n|\r|\n/)
55
+ split_routes = routes.split(/\r\n|\r|\n/).reject { |route| excluded_route?(route) }
37
56
  parse_routes = split_routes.map do |route|
38
57
  "# #{route}\n"
39
58
  end
@@ -42,14 +61,23 @@ module AwesomeAnnotate
42
61
  parse_routes.join
43
62
  end
44
63
 
64
+ def excluded_route?(route)
65
+ @exclude_routes.any? { |pattern| File.fnmatch?(pattern, route.strip) }
66
+ end
67
+
45
68
  def insert_file_before_class(file_path, message)
46
- insert_into_file file_path, :before => "Rails.application.routes.draw do\n" do
47
- message
48
- end
69
+ replace_or_insert_annotation(
70
+ file_path: file_path,
71
+ marker: 'routes',
72
+ content: message,
73
+ before: "Rails.application.routes.draw do\n",
74
+ position: @annotation_position
75
+ )
49
76
  end
50
77
 
51
78
  def self.source_root
52
79
  Dir.pwd
53
80
  end
81
+ private_class_method :source_root
54
82
  end
55
83
  end
@@ -0,0 +1,100 @@
1
+ # frozen_string_literal: true
2
+
3
+ module AwesomeAnnotate
4
+ module SchemaAnnotation
5
+ private
6
+
7
+ def schema_annotation(klass)
8
+ columns = schema_columns(klass)
9
+
10
+ [
11
+ schema_header(klass),
12
+ column_annotations(klass, columns),
13
+ include_indexes? ? index_annotations(klass) : '',
14
+ "#\n"
15
+ ].join
16
+ end
17
+
18
+ def include_indexes?
19
+ @include_indexes != false
20
+ end
21
+
22
+ def include_column_defaults?
23
+ @include_column_defaults != false
24
+ end
25
+
26
+ def schema_columns(klass)
27
+ klass.columns.reject { |column| @exclude_columns.include?(column.name) }
28
+ end
29
+
30
+ def schema_header(klass)
31
+ [
32
+ "# == Schema Information\n",
33
+ "#\n",
34
+ "# Table name: #{klass.table_name}\n",
35
+ "#\n"
36
+ ].join
37
+ end
38
+
39
+ def column_annotations(klass, columns)
40
+ column_name_width = columns.map { |column| column.name.length }.max || 0
41
+ column_type_width = columns.map { |column| column_type(column).length }.max || 0
42
+
43
+ columns.map { |column| column_annotation(klass, column, column_name_width, column_type_width) }.join
44
+ end
45
+
46
+ def column_annotation(klass, column, column_name_width, column_type_width)
47
+ column_name = column.name.ljust(column_name_width)
48
+ type = column_type(column).ljust(column_type_width)
49
+ details = column_details(klass, column)
50
+ line = "# #{column_name} :#{type}"
51
+
52
+ line = "#{line} #{details.join(', ')}" if details.any?
53
+ "#{line}\n"
54
+ end
55
+
56
+ def column_type(column)
57
+ column.type.to_s
58
+ end
59
+
60
+ def column_details(klass, column)
61
+ details = []
62
+ details << 'not null' if column.null == false
63
+ details << 'primary key' if column.name == klass.primary_key
64
+ details << "default(#{column.default.inspect})" if include_column_defaults? && !column.default.nil?
65
+ details
66
+ end
67
+
68
+ def index_annotations(klass)
69
+ indexes = klass.connection.indexes(klass.table_name)
70
+ return '' if indexes.empty?
71
+
72
+ index_column_width = indexes.map { |index| index_columns(index).length }.max || 0
73
+
74
+ [
75
+ "#\n",
76
+ "# Indexes\n",
77
+ "#\n",
78
+ indexes.map { |index| index_annotation(index, index_column_width) }.join
79
+ ].join
80
+ end
81
+
82
+ def index_annotation(index, index_column_width)
83
+ columns = index_columns(index).ljust(index_column_width)
84
+ details = index_details(index)
85
+
86
+ "# #{columns} #{details.join(', ')}\n"
87
+ end
88
+
89
+ def index_columns(index)
90
+ "(#{index.columns.join(',')})"
91
+ end
92
+
93
+ def index_details(index)
94
+ details = []
95
+ details << 'UNIQUE' if index.unique
96
+ details << index.name
97
+ details
98
+ end
99
+ end
100
+ end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module AwesomeAnnotate
4
- VERSION = "0.1.5"
4
+ VERSION = '0.3.1'
5
5
  end
@@ -1,8 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative "awesome_annotate/version"
4
- require_relative "awesome_annotate/cli"
5
-
6
- module AwesomeAnnotate
7
- class Error < StandardError; end
8
- end
3
+ require_relative 'awesome_annotate/version'
4
+ require_relative 'awesome_annotate/error'
5
+ require_relative 'awesome_annotate/configuration'
6
+ require_relative 'awesome_annotate/cli'
metadata CHANGED
@@ -1,65 +1,74 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: awesome_annotate
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.5
4
+ version: 0.3.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - wisdom-plus
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2024-09-03 00:00:00.000000000 Z
11
+ date: 2026-05-31 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
- name: thor
14
+ name: activerecord
15
15
  requirement: !ruby/object:Gem::Requirement
16
16
  requirements:
17
17
  - - ">="
18
18
  - !ruby/object:Gem::Version
19
- version: '0'
19
+ version: '6.1'
20
+ - - "<"
21
+ - !ruby/object:Gem::Version
22
+ version: '8.0'
20
23
  type: :runtime
21
24
  prerelease: false
22
25
  version_requirements: !ruby/object:Gem::Requirement
23
26
  requirements:
24
27
  - - ">="
25
28
  - !ruby/object:Gem::Version
26
- version: '0'
29
+ version: '6.1'
30
+ - - "<"
31
+ - !ruby/object:Gem::Version
32
+ version: '8.0'
27
33
  - !ruby/object:Gem::Dependency
28
- name: activerecord
34
+ name: thor
29
35
  requirement: !ruby/object:Gem::Requirement
30
36
  requirements:
31
- - - ">="
37
+ - - "~>"
32
38
  - !ruby/object:Gem::Version
33
- version: 6.1.0
39
+ version: '1.3'
34
40
  type: :runtime
35
41
  prerelease: false
36
42
  version_requirements: !ruby/object:Gem::Requirement
37
43
  requirements:
38
- - - ">="
44
+ - - "~>"
39
45
  - !ruby/object:Gem::Version
40
- version: 6.1.0
46
+ version: '1.3'
41
47
  description: annotate your code with comments (e.g. model schema, routes, etc.)
42
48
  email:
43
- - wisdom.plus.264.dev@gmail.com
49
+ - wisdom-plus@users.noreply.github.com
44
50
  executables:
45
51
  - awesome_annotate
46
52
  extensions: []
47
53
  extra_rdoc_files: []
48
54
  files:
49
55
  - ".rspec"
50
- - ".rspec_status"
51
56
  - ".rubocop.yml"
57
+ - ".ruby-version"
52
58
  - CHANGELOG.md
53
59
  - LICENSE.txt
54
60
  - README.md
55
61
  - Rakefile
56
- - awesome_annotate.gemspec
57
62
  - exe/awesome_annotate
58
63
  - lib/awesome_annotate.rb
64
+ - lib/awesome_annotate/annotation_block.rb
59
65
  - lib/awesome_annotate/cli.rb
66
+ - lib/awesome_annotate/configuration.rb
60
67
  - lib/awesome_annotate/error.rb
61
68
  - lib/awesome_annotate/model.rb
69
+ - lib/awesome_annotate/rails_environment.rb
62
70
  - lib/awesome_annotate/route.rb
71
+ - lib/awesome_annotate/schema_annotation.rb
63
72
  - lib/awesome_annotate/version.rb
64
73
  - sig/awesome_annotate.rbs
65
74
  homepage: https://github.com/wisdom-plus/awesome_annotate
@@ -70,6 +79,7 @@ metadata:
70
79
  homepage_uri: https://github.com/wisdom-plus/awesome_annotate
71
80
  source_code_uri: https://github.com/wisdom-plus/awesome_annotate
72
81
  changelog_uri: https://github.com/wisdom-plus/awesome_annotate/blob/main/CHANGELOG.md
82
+ rubygems_mfa_required: 'true'
73
83
  post_install_message:
74
84
  rdoc_options: []
75
85
  require_paths:
@@ -78,14 +88,17 @@ required_ruby_version: !ruby/object:Gem::Requirement
78
88
  requirements:
79
89
  - - ">="
80
90
  - !ruby/object:Gem::Version
81
- version: 3.0.0
91
+ version: '3.0'
92
+ - - "<"
93
+ - !ruby/object:Gem::Version
94
+ version: '4.0'
82
95
  required_rubygems_version: !ruby/object:Gem::Requirement
83
96
  requirements:
84
97
  - - ">="
85
98
  - !ruby/object:Gem::Version
86
99
  version: '0'
87
100
  requirements: []
88
- rubygems_version: 3.5.3
101
+ rubygems_version: 3.4.10
89
102
  signing_key:
90
103
  specification_version: 4
91
104
  summary: annotate your code with comments
data/.rspec_status DELETED
@@ -1,11 +0,0 @@
1
- example_id | status | run_time |
2
- ---------------------------------------------------- | ------ | --------------- |
3
- ./spec/awesome_annotate_spec.rb[1:1] | passed | 0.00031 seconds |
4
- ./spec/lib/awesome_annotate/cli_spec.rb[1:3:1] | passed | 0.00185 seconds |
5
- ./spec/lib/awesome_annotate/model_spec.rb[1:1:1:1:1] | passed | 0.19755 seconds |
6
- ./spec/lib/awesome_annotate/model_spec.rb[1:1:1:2:1] | passed | 0.00078 seconds |
7
- ./spec/lib/awesome_annotate/model_spec.rb[1:1:1:3:1] | passed | 0.00018 seconds |
8
- ./spec/lib/awesome_annotate/model_spec.rb[1:1:2:1] | passed | 0.00008 seconds |
9
- ./spec/lib/awesome_annotate/route_spec.rb[1:1:1:1:1] | passed | 0.00303 seconds |
10
- ./spec/lib/awesome_annotate/route_spec.rb[1:1:1:2:1] | passed | 0.0005 seconds |
11
- ./spec/lib/awesome_annotate/route_spec.rb[1:1:2:1] | passed | 0.00009 seconds |
@@ -1,36 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require_relative "lib/awesome_annotate/version"
4
-
5
- Gem::Specification.new do |spec|
6
- spec.name = "awesome_annotate"
7
- spec.version = AwesomeAnnotate::VERSION
8
- spec.authors = ["wisdom-plus"]
9
- spec.email = ["wisdom.plus.264.dev@gmail.com"]
10
-
11
- spec.summary = "annotate your code with comments"
12
- spec.description = "annotate your code with comments (e.g. model schema, routes, etc.)"
13
- spec.homepage = "https://github.com/wisdom-plus/awesome_annotate"
14
- spec.license = "MIT"
15
- spec.required_ruby_version = ">= 3.0.0"
16
-
17
- spec.metadata["allowed_push_host"] = "https://rubygems.org"
18
-
19
- spec.metadata["homepage_uri"] = spec.homepage
20
- spec.metadata["source_code_uri"] = spec.homepage
21
- spec.metadata["changelog_uri"] = spec.homepage + "/blob/main/CHANGELOG.md"
22
-
23
- # Specify which files should be added to the gem when it is released.
24
- # The `git ls-files -z` loads the files in the RubyGem that have been added into git.
25
- spec.files = Dir.chdir(__dir__) do
26
- `git ls-files -z`.split("\x0").reject do |f|
27
- (File.expand_path(f) == __FILE__) ||
28
- f.start_with?(*%w[bin/ test/ spec/ features/ .git .github appveyor Gemfile])
29
- end
30
- end
31
- spec.bindir = "exe"
32
- spec.executables = spec.files.grep(%r{\Aexe/}) { |f| File.basename(f) }
33
- spec.require_paths = ["lib"]
34
- spec.add_dependency "thor"
35
- spec.add_dependency "activerecord", ">= 6.1.0"
36
- end