annotaterb 4.23.0 → 4.25.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 (31) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +100 -3
  3. data/README.md +129 -9
  4. data/VERSION +1 -1
  5. data/lib/annotate_rb/model_annotator/annotation/annotation_builder.rb +8 -1
  6. data/lib/annotate_rb/model_annotator/column_annotation/default_value_builder.rb +1 -0
  7. data/lib/annotate_rb/model_annotator/column_annotation/enum_default.rb +11 -0
  8. data/lib/annotate_rb/model_annotator/column_annotation.rb +1 -0
  9. data/lib/annotate_rb/model_annotator/exclusion_constraint_annotation/annotation.rb +40 -0
  10. data/lib/annotate_rb/model_annotator/exclusion_constraint_annotation/annotation_builder.rb +38 -0
  11. data/lib/annotate_rb/model_annotator/exclusion_constraint_annotation/exclusion_constraint_component.rb +27 -0
  12. data/lib/annotate_rb/model_annotator/exclusion_constraint_annotation.rb +11 -0
  13. data/lib/annotate_rb/model_annotator/file_parser/annotation_finder.rb +38 -2
  14. data/lib/annotate_rb/model_annotator/file_parser/yml_parser.rb +44 -5
  15. data/lib/annotate_rb/model_annotator/foreign_key_annotation/foreign_key_component_builder.rb +7 -1
  16. data/lib/annotate_rb/model_annotator/index_annotation/annotation_builder.rb +29 -0
  17. data/lib/annotate_rb/model_annotator/index_annotation/index_component.rb +15 -5
  18. data/lib/annotate_rb/model_annotator/model_wrapper.rb +63 -1
  19. data/lib/annotate_rb/model_annotator/project_annotation_remover.rb +3 -1
  20. data/lib/annotate_rb/model_annotator/related_files_list_builder.rb +27 -2
  21. data/lib/annotate_rb/model_annotator/single_file_annotation_remover.rb +1 -1
  22. data/lib/annotate_rb/model_annotator/single_file_annotator.rb +1 -1
  23. data/lib/annotate_rb/model_annotator/unique_constraint_annotation/annotation.rb +40 -0
  24. data/lib/annotate_rb/model_annotator/unique_constraint_annotation/annotation_builder.rb +37 -0
  25. data/lib/annotate_rb/model_annotator/unique_constraint_annotation/unique_constraint_component.rb +27 -0
  26. data/lib/annotate_rb/model_annotator/unique_constraint_annotation.rb +11 -0
  27. data/lib/annotate_rb/model_annotator.rb +2 -0
  28. data/lib/annotate_rb/options.rb +11 -4
  29. data/lib/annotate_rb/parser.rb +21 -1
  30. data/lib/annotate_rb/runner.rb +0 -2
  31. metadata +10 -1
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 249172f3c37299ac55df233909754c48456b629d11517ce03c84fae62232677d
4
- data.tar.gz: 9fa726c0abbb3bc7508257d24e7e0f0e4f9a79c94e255909b8bf7162a2f25d9f
3
+ metadata.gz: e76c98c2c18b7021f7844189a076f4d5cb52b61777c5200bff9888497d3924d2
4
+ data.tar.gz: ddf7481ef3fdb08071855c9131a7dfca2426d29ad0c7dfdeff9f891d510a867b
5
5
  SHA512:
6
- metadata.gz: f695ccb014c329b7c897f1832c4bb053df9fbec718198d68466884585b17f17fbfb6805a053024a38133196f7d9934aaff56d89c83598160966b1fc4dcaaaa3c
7
- data.tar.gz: 73994c40f74a0c793cd974cf1369b59403e3b3a49ce6c643b661f9dfdffb7425dcfbb96c98ba2a23c3347861fa4cd759be380613d92627cb4046956af249dea3
6
+ metadata.gz: 38dd0ae99e1a1e21dd68963b5e6f85bb72d620955ab001df8136dc4d40122196d4bb88dd107bfe95fdd93bcb17befe907f4ebe001ad05f364067b17d40c839e3
7
+ data.tar.gz: be47e90cad90aefdd1ea96d494783731270b26c5a8f24843912eea6f418a1375bbbc7bdc3550181f763eef47eb46eb595d1e54fc2bc06ede1eb26abfbfeb917b
data/CHANGELOG.md CHANGED
@@ -1,9 +1,108 @@
1
1
  # Changelog
2
2
 
3
+ ## [v4.24.0](https://github.com/drwl/annotaterb/tree/v4.24.0) (2026-07-24)
4
+
5
+ [Full Changelog](https://github.com/drwl/annotaterb/compare/v4.23.0...v4.24.0)
6
+
7
+ **Implemented enhancements:**
8
+
9
+ - Annotate postgres enums [\#309](https://github.com/drwl/annotaterb/issues/309)
10
+ - `--frozen` option returns a non-zero error code when database is inaccessible [\#240](https://github.com/drwl/annotaterb/issues/240)
11
+ - Add support for printing enum types [\#176](https://github.com/drwl/annotaterb/issues/176)
12
+
13
+ **Fixed bugs:**
14
+
15
+ - Annotation placement for namespaced models is inconsistent — anchor shifts with file shape \(doc comment presence\) [\#366](https://github.com/drwl/annotaterb/issues/366)
16
+ - Fixture files that start with \<% \(erb\) insert doc inside the erb block [\#345](https://github.com/drwl/annotaterb/issues/345)
17
+ - Schema information inserted every run into some fixture files. [\#344](https://github.com/drwl/annotaterb/issues/344)
18
+
19
+ **Closed issues:**
20
+
21
+ - Routes are not annotated after migration tasks [\#251](https://github.com/drwl/annotaterb/issues/251)
22
+
23
+ **Merged pull requests:**
24
+
25
+ - Bump version to v4.24.0 [\#371](https://github.com/drwl/annotaterb/pull/371) ([drwl](https://github.com/drwl))
26
+ - Revert "Release v4.24.0" [\#370](https://github.com/drwl/annotaterb/pull/370) ([drwl](https://github.com/drwl))
27
+ - Show DEFERRABLE INITIALLY on foreign keys [\#365](https://github.com/drwl/annotaterb/pull/365) ([kamipo](https://github.com/kamipo))
28
+ - Annotate PostgreSQL unique and exclusion constraints [\#364](https://github.com/drwl/annotaterb/pull/364) ([kamipo](https://github.com/kamipo))
29
+ - Fix `--without-comment` help text to say exclude [\#363](https://github.com/drwl/annotaterb/pull/363) ([OdenTakashi](https://github.com/OdenTakashi))
30
+ - Fix NoMethodError when handling malformed annotations [\#362](https://github.com/drwl/annotaterb/pull/362) ([OdenTakashi](https://github.com/OdenTakashi))
31
+ - Remove empty TODO comment from Runner [\#361](https://github.com/drwl/annotaterb/pull/361) ([OdenTakashi](https://github.com/OdenTakashi))
32
+ - Move ignore\_database\_name check into AnnotationBuilder [\#360](https://github.com/drwl/annotaterb/pull/360) ([OdenTakashi](https://github.com/OdenTakashi))
33
+ - Add YAML configuration option reference to the README. [\#359](https://github.com/drwl/annotaterb/pull/359) ([OdenTakashi](https://github.com/OdenTakashi))
34
+ - Respect DB column defaults over `attribute :foo, default: X` overrides [\#358](https://github.com/drwl/annotaterb/pull/358) ([kamipo](https://github.com/kamipo))
35
+ - added :ignore\_database\_name option [\#357](https://github.com/drwl/annotaterb/pull/357) ([HoneyryderChuck](https://github.com/HoneyryderChuck))
36
+ - Add Markdown annotation idempotency test [\#356](https://github.com/drwl/annotaterb/pull/356) ([aouxwoux](https://github.com/aouxwoux))
37
+ - Fix schema\_like? to recognize markdown-formatted annotation rows [\#354](https://github.com/drwl/annotaterb/pull/354) ([nashirox](https://github.com/nashirox))
38
+ - Keep fixture annotations out of ERB blocks \(\#345\) [\#353](https://github.com/drwl/annotaterb/pull/353) ([Halvanhelv](https://github.com/Halvanhelv))
39
+ - Stop tracking generated secondary DB artifacts in dummyapp [\#352](https://github.com/drwl/annotaterb/pull/352) ([OdenTakashi](https://github.com/OdenTakashi))
40
+ - Add Rails version matrix to CI [\#351](https://github.com/drwl/annotaterb/pull/351) ([OdenTakashi](https://github.com/OdenTakashi))
41
+ - Create release script [\#349](https://github.com/drwl/annotaterb/pull/349) ([drwl](https://github.com/drwl))
42
+ - Generate changelog for v4.23.0 [\#348](https://github.com/drwl/annotaterb/pull/348) ([drwl](https://github.com/drwl))
43
+
44
+ ## [v4.23.0](https://github.com/drwl/annotaterb/tree/v4.23.0) (2026-06-25)
45
+
46
+ [Full Changelog](https://github.com/drwl/annotaterb/compare/v4.22.0...v4.23.0)
47
+
48
+ **Implemented enhancements:**
49
+
50
+ - \[Request\] Support for multiple serializers for one model [\#329](https://github.com/drwl/annotaterb/issues/329)
51
+ - Support COMMENT on an INDEX [\#258](https://github.com/drwl/annotaterb/issues/258)
52
+
53
+ **Fixed bugs:**
54
+
55
+ - Re-running annotaterb strips indentation from nested \(indented\) schema annotations [\#332](https://github.com/drwl/annotaterb/issues/332)
56
+ - Setting all sort options to `false` sometimes doesn't respect migration/DB order [\#237](https://github.com/drwl/annotaterb/issues/237)
57
+
58
+ **Closed issues:**
59
+
60
+ - mistake post please delete this [\#314](https://github.com/drwl/annotaterb/issues/314)
61
+ - Removal of final empty comment line after foreign keys results in duplicate `fk_rails_...` line [\#297](https://github.com/drwl/annotaterb/issues/297)
62
+ - Default settings does not indent correctly inside a module [\#271](https://github.com/drwl/annotaterb/issues/271)
63
+ - Top does not do what I think it should? And annotator fails to indent. [\#202](https://github.com/drwl/annotaterb/issues/202)
64
+ - annotaterb is moving my class doc [\#201](https://github.com/drwl/annotaterb/issues/201)
65
+
66
+ **Merged pull requests:**
67
+
68
+ - Bump version to v4.23.0 [\#347](https://github.com/drwl/annotaterb/pull/347) ([drwl](https://github.com/drwl))
69
+ - Bump actions/checkout from 6 to 7 [\#346](https://github.com/drwl/annotaterb/pull/346) ([dependabot[bot]](https://github.com/apps/dependabot))
70
+ - Fix duplicate annotations when a file ends with a trailing blank line [\#342](https://github.com/drwl/annotaterb/pull/342) ([OdenTakashi](https://github.com/OdenTakashi))
71
+ - Fix update\_config for non-default config locations [\#341](https://github.com/drwl/annotaterb/pull/341) ([OdenTakashi](https://github.com/OdenTakashi))
72
+ - Fix NameError in ZeitwerkClassGetter for collapsed Zeitwerk paths [\#339](https://github.com/drwl/annotaterb/pull/339) ([KaiyuanMa](https://github.com/KaiyuanMa))
73
+ - Fix annotation inserted inside module when doc comment is present [\#338](https://github.com/drwl/annotaterb/pull/338) ([OdenTakashi](https://github.com/OdenTakashi))
74
+ - Add `position_in_class: after_doc` to keep class doc adjacent to class [\#337](https://github.com/drwl/annotaterb/pull/337) ([yamat47](https://github.com/yamat47))
75
+ - Fix annotation placement above inner class declarations under `nested_position` [\#336](https://github.com/drwl/annotaterb/pull/336) ([yamat47](https://github.com/yamat47))
76
+ - Drop Ruby \< 3.3 from CI matrix [\#335](https://github.com/drwl/annotaterb/pull/335) ([drwl](https://github.com/drwl))
77
+ - Fix annotation placement in nested files and around trailing user comments [\#334](https://github.com/drwl/annotaterb/pull/334) ([yamat47](https://github.com/yamat47))
78
+ - Preserve indentation when re-running annotaterb on nested-position annotations. [\#333](https://github.com/drwl/annotaterb/pull/333) ([yamat47](https://github.com/yamat47))
79
+ - Respect `--config-path` during config loading [\#331](https://github.com/drwl/annotaterb/pull/331) ([OdenTakashi](https://github.com/OdenTakashi))
80
+ - Fall back to current task when no top-level task is set [\#328](https://github.com/drwl/annotaterb/pull/328) ([jordan-brough](https://github.com/jordan-brough))
81
+ - Fix --show-migration option not working on Rails 7.2+. [\#327](https://github.com/drwl/annotaterb/pull/327) ([OdenTakashi](https://github.com/OdenTakashi))
82
+ - Exit with non-zero code when database is not accessible [\#326](https://github.com/drwl/annotaterb/pull/326) ([OdenTakashi](https://github.com/OdenTakashi))
83
+ - fix potential ReDoS in route annotator regex [\#325](https://github.com/drwl/annotaterb/pull/325) ([OdenTakashi](https://github.com/OdenTakashi))
84
+ - Support opt-in display of PostgreSQL enum types in annotations [\#324](https://github.com/drwl/annotaterb/pull/324) ([OdenTakashi](https://github.com/OdenTakashi))
85
+ - Fix error when column comments contain `%` [\#322](https://github.com/drwl/annotaterb/pull/322) ([willnet](https://github.com/willnet))
86
+ - Add unit tests for AnnotateRb::Helper [\#321](https://github.com/drwl/annotaterb/pull/321) ([OdenTakashi](https://github.com/OdenTakashi))
87
+ - Migrate issue templates to folder-based format [\#320](https://github.com/drwl/annotaterb/pull/320) ([OdenTakashi](https://github.com/OdenTakashi))
88
+ - Add opt-in show\_indexes\_comments option to display index comments [\#319](https://github.com/drwl/annotaterb/pull/319) ([olleolleolle](https://github.com/olleolleolle))
89
+ - Only connect SecondaryRecord to secondary DB when MULTI\_DB\_TEST is set [\#318](https://github.com/drwl/annotaterb/pull/318) ([OdenTakashi](https://github.com/OdenTakashi))
90
+ - Correct secondary database setup in dummy app [\#317](https://github.com/drwl/annotaterb/pull/317) ([OdenTakashi](https://github.com/OdenTakashi))
91
+ - Remove duplicate ignore\_unknown\_models option definition [\#316](https://github.com/drwl/annotaterb/pull/316) ([OdenTakashi](https://github.com/OdenTakashi))
92
+ - Fix column annotation alignment for CJK and fullwidth characters [\#315](https://github.com/drwl/annotaterb/pull/315) ([SergeyGildenshtern](https://github.com/SergeyGildenshtern))
93
+ - Added command example for applying annotations to README [\#313](https://github.com/drwl/annotaterb/pull/313) ([zackerms](https://github.com/zackerms))
94
+ - Generate changelog for v4.22.0 [\#311](https://github.com/drwl/annotaterb/pull/311) ([drwl](https://github.com/drwl))
95
+ - Add opt-in support for automatically annotating routes after migration tasks [\#293](https://github.com/drwl/annotaterb/pull/293) ([OdenTakashi](https://github.com/OdenTakashi))
96
+
3
97
  ## [v4.22.0](https://github.com/drwl/annotaterb/tree/v4.22.0) (2026-02-12)
4
98
 
5
99
  [Full Changelog](https://github.com/drwl/annotaterb/compare/v4.21.0...v4.22.0)
6
100
 
101
+ **Implemented enhancements:**
102
+
103
+ - Feature: ruby-lsp addon [\#175](https://github.com/drwl/annotaterb/issues/175)
104
+ - Mounting ActionCable leads to weird annotation [\#161](https://github.com/drwl/annotaterb/issues/161)
105
+
7
106
  **Fixed bugs:**
8
107
 
9
108
  - Yardoc formatting for comments on database attributes [\#162](https://github.com/drwl/annotaterb/issues/162)
@@ -13,8 +112,6 @@
13
112
  - New `ignore_multi_database_name` option seems to be non-functional [\#303](https://github.com/drwl/annotaterb/issues/303)
14
113
  - Changing sort options does not change annotations [\#294](https://github.com/drwl/annotaterb/issues/294)
15
114
  - CLI script for annotaterb not installed or runnable [\#290](https://github.com/drwl/annotaterb/issues/290)
16
- - Feature: ruby-lsp addon [\#175](https://github.com/drwl/annotaterb/issues/175)
17
- - Mounting ActionCable leads to weird annotation [\#161](https://github.com/drwl/annotaterb/issues/161)
18
115
 
19
116
  **Merged pull requests:**
20
117
 
@@ -22,7 +119,7 @@
22
119
  - Run CI on CRuby 4.0 [\#308](https://github.com/drwl/annotaterb/pull/308) ([viralpraxis](https://github.com/viralpraxis))
23
120
  - Generate changelog for v4.21.0 [\#307](https://github.com/drwl/annotaterb/pull/307) ([drwl](https://github.com/drwl))
24
121
  - fix NoMethodError when using nested\_position with fixture files [\#298](https://github.com/drwl/annotaterb/pull/298) ([OdenTakashi](https://github.com/OdenTakashi))
25
- - fix: Respect configured sort [\#295](https://github.com/drwl/annotaterb/pull/295) ([patrickarnett](https://github.com/patrickarnett)) **(Maintainer note: this could result in annotations shifting depending on configuration, please create an issue if it is a breaking change)**
122
+ - fix: Respect configured sort [\#295](https://github.com/drwl/annotaterb/pull/295) ([patrickarnett](https://github.com/patrickarnett))
26
123
  - Use `#lease_connection` if available [\#292](https://github.com/drwl/annotaterb/pull/292) ([viralpraxis](https://github.com/viralpraxis))
27
124
  - refactor: simplify primary key check logic \(no functional changes\) [\#285](https://github.com/drwl/annotaterb/pull/285) ([OdenTakashi](https://github.com/OdenTakashi))
28
125
  - Honor skip\_on\_db\_migrate config option when runnig migrate tasks [\#274](https://github.com/drwl/annotaterb/pull/274) ([martinechtner](https://github.com/martinechtner))
data/README.md CHANGED
@@ -22,16 +22,17 @@ The schema comment looks like this:
22
22
  ```ruby
23
23
  # == Schema Information
24
24
  #
25
- # Table name: tasks
25
+ # Table name: users
26
26
  #
27
- # id :integer not null, primary key
28
- # content :string
29
- # count :integer
30
- # status :boolean
31
- # created_at :datetime not null
32
- # updated_at :datetime not null
27
+ # id :integer not null, primary key
28
+ # name :string
29
+ # email :string
30
+ # sign_in_count :integer default(0), not null
31
+ # admin :boolean default(FALSE), not null
32
+ # created_at :datetime not null
33
+ # updated_at :datetime not null
33
34
  #
34
- class Task < ApplicationRecord
35
+ class User < ApplicationRecord
35
36
  ...
36
37
  ```
37
38
 
@@ -68,7 +69,7 @@ This will copy a rake task into your Rails project's `lib/tasks` directory that
68
69
  $ bin/rails db:migrate
69
70
  # ...
70
71
  # Annotating models
71
- # Annotated (1): app/models/task.rb
72
+ # Annotated (1): app/models/user.rb
72
73
  ```
73
74
 
74
75
  To skip the automatic annotation that happens after a db task, pass the environment variable `ANNOTATERB_SKIP_ON_DB_TASKS=1` before your command.
@@ -158,6 +159,8 @@ Annotate model options:
158
159
  --without-column-comments exclude column comments in model annotations
159
160
  --position-of-column-comment [with_name|rightmost_column]
160
161
  set the position, in the annotation block, of the column comment
162
+ --enum-default-format [label|raw|both]
163
+ set how defaults of enum backed columns are shown
161
164
  --with-table-comments include table comments in model annotations
162
165
  --without-table-comments exclude table comments in model annotations
163
166
  --classes-default-to-s class Custom classes to be represented with `to_s`, may be used multiple times
@@ -237,6 +240,123 @@ Annotaterb reads first the configuration file, if it exists, passes its content
237
240
 
238
241
  For further details visit the [section in the migration guide](MIGRATION_GUIDE.md#automatic-annotations-after-running-database-migration-commands).
239
242
 
243
+ ### Configuration options
244
+
245
+ Keys use snake_case and match the gem defaults in `AnnotateRb::Options`. CLI flags override values from the config file.
246
+
247
+ #### Position
248
+
249
+ | Option | Default | Description |
250
+ | --- | --- | --- |
251
+ | `position` | `before` | Fallback position for all `position_in_*` options. One of `before`, `top`, `after`, `bottom`, `before_doc`. |
252
+ | `position_in_class` | `before` | Position in model files. `before_doc` keeps class documentation adjacent to the class. |
253
+ | `position_in_factory` | `before` | Position in FactoryBot factory files. |
254
+ | `position_in_fixture` | `before` | Position in fixture files. |
255
+ | `position_in_test` | `before` | Position in test/spec files. |
256
+ | `position_in_routes` | `before` | Position in `config/routes.rb`. |
257
+ | `position_in_serializer` | `before` | Position in serializer files. |
258
+ | `position_in_additional_file_patterns` | `before` | Position in files matched by `additional_file_patterns`. |
259
+ | `nested_position` | `false` | Place annotations directly above nested classes/modules instead of at the top of the file. |
260
+
261
+ #### Schema annotation content
262
+
263
+ | Option | Default | Description |
264
+ | --- | --- | --- |
265
+ | `show_foreign_keys` | `true` | List foreign key constraints. |
266
+ | `show_complete_foreign_keys` | `false` | Use complete foreign key names. |
267
+ | `show_indexes` | `true` | List table indexes. |
268
+ | `show_indexes_comments` | `false` | Include index comments. |
269
+ | `show_indexes_include` | `false` | Include `INCLUDE` columns on indexes. |
270
+ | `simple_indexes` | `false` | Concat related indexes onto each column line. |
271
+ | `show_check_constraints` | `false` | List check constraints. |
272
+ | `show_enums` | `false` | Show PostgreSQL enum types. |
273
+ | `enum_default_format` | `label` | Default shown for enum backed columns: `label` (`default("idnow")`), `raw` (`default(0)`) or `both` (`default(0: "idnow")`). |
274
+ | `show_virtual_columns` | `false` | Show virtual/generated columns. |
275
+ | `include_version` | `false` | Include the migration version number. |
276
+ | `with_comment` | `true` | Include database comments (fallback for column/table comment flags). |
277
+ | `with_column_comments` | `true` | Include column comments. |
278
+ | `with_table_comments` | `true` | Include table comments. |
279
+ | `position_of_column_comment` | `with_name` | Column comment placement: `with_name` or `rightmost_column`. |
280
+ | `hide_default_column_types` | `""` | Comma-separated column types that omit defaults (e.g. `json,jsonb,hstore`). |
281
+ | `hide_limit_column_types` | `""` | Comma-separated column types that omit limits (e.g. `integer,boolean,text`). |
282
+ | `ignore_columns` | `null` | Regex of column names to skip. |
283
+ | `ignore_database_name` | `false` | Omit the database name from annotations. |
284
+ | `ignore_multi_database_name` | `false` | Omit the database name in multi-database setups. |
285
+ | `timestamp_columns` | `[created_at, updated_at]` | Column names treated as timestamps for classified sorting. |
286
+ | `classes_default_to_s` | `[]` | Class names whose default values are rendered with `to_s`. |
287
+
288
+ #### Sorting and format
289
+
290
+ | Option | Default | Description |
291
+ | --- | --- | --- |
292
+ | `sort` | `false` | Sort columns alphabetically. |
293
+ | `classified_sort` | `true` | Sort as id → other columns → timestamps → associations. |
294
+ | `grouped_polymorphic` | `false` | Group polymorphic associations when using `classified_sort`. |
295
+ | `format_markdown` | `false` | Render annotations as Markdown. |
296
+ | `format_rdoc` | `false` | Render annotations as RDoc. |
297
+ | `format_yard` | `false` | Render annotations as YARD. |
298
+
299
+ #### What to annotate / exclude
300
+
301
+ | Option | Default | Description |
302
+ | --- | --- | --- |
303
+ | `active_admin` | `false` | Annotate ActiveAdmin models. |
304
+ | `exclude_controllers` | `true` | Skip controller files. |
305
+ | `exclude_factories` | `false` | Skip factory files. |
306
+ | `exclude_fixtures` | `false` | Skip fixture files. |
307
+ | `exclude_helpers` | `true` | Skip helper files. |
308
+ | `exclude_scaffolds` | `true` | Skip scaffold files. |
309
+ | `exclude_serializers` | `false` | Skip serializer files. |
310
+ | `exclude_sti_subclasses` | `false` | Skip STI subclasses. |
311
+ | `exclude_tests` | `false` | Skip test/spec files. Can also be an array of symbols. |
312
+ | `ignore_model_sub_dir` | `false` | Ignore subdirectories under `model_dir`. |
313
+ | `ignore_unknown_models` | `false` | Suppress warnings for bad model files. |
314
+
315
+ #### Paths
316
+
317
+ | Option | Default | Description |
318
+ | --- | --- | --- |
319
+ | `model_dir` | `[app/models]` | Directories containing models. |
320
+ | `root_dir` | `[""]` | Root directories for multi-project layouts. |
321
+ | `additional_file_patterns` | `[]` | Extra paths/globs to annotate (supports `%MODEL_NAME%`). |
322
+ | `require` | `[]` | Extra files to require before loading models. |
323
+
324
+ #### Routes
325
+
326
+ | Option | Default | Description |
327
+ | --- | --- | --- |
328
+ | `ignore_routes` | `null` | Regex of routes to skip. |
329
+ | `timestamp` | `false` | Include a timestamp in route annotations. |
330
+ | `auto_annotate_routes_after_migrate` | `false` | Also annotate routes after DB migrate tasks. |
331
+
332
+ #### Wrappers and behavior
333
+
334
+ | Option | Default | Description |
335
+ | --- | --- | --- |
336
+ | `wrapper` | `null` | Text used for both opening and closing wrappers. |
337
+ | `wrapper_open` | `null` | Opening wrapper text (falls back to `wrapper`). |
338
+ | `wrapper_close` | `null` | Closing wrapper text (falls back to `wrapper`). |
339
+ | `force` | `false` | Rewrite annotations even when unchanged. |
340
+ | `frozen` | `false` | Exit non-zero if annotations would change. |
341
+ | `skip_on_db_migrate` | `false` | Skip automatic annotation after DB migrate tasks. |
342
+ | `debug` | `false` | Print resolved options for debugging. |
343
+ | `trace` | `false` | Print full stack traces when annotation fails. |
344
+
345
+ Example:
346
+
347
+ ```yml
348
+ # .annotaterb.yml
349
+ position: after
350
+ show_foreign_keys: true
351
+ show_indexes: true
352
+ show_enums: true
353
+ classified_sort: true
354
+ model_dir:
355
+ - app/models
356
+ - app/models/concerns
357
+ skip_on_db_migrate: false
358
+ ```
359
+
240
360
  ### Preserving class documentation comments
241
361
 
242
362
  By default, when `position_in_class` is `before` (or `top`), AnnotateRb places the schema annotation immediately before the class declaration line. Any human-written documentation comment that was directly above the class is therefore pushed above the annotation.
data/VERSION CHANGED
@@ -1 +1 @@
1
- 4.23.0
1
+ 4.25.0
@@ -27,6 +27,8 @@ module AnnotateRb
27
27
  IndexAnnotation::AnnotationBuilder.new(@model, @options).build,
28
28
  ForeignKeyAnnotation::AnnotationBuilder.new(@model, @options).build,
29
29
  CheckConstraintAnnotation::AnnotationBuilder.new(@model, @options).build,
30
+ UniqueConstraintAnnotation::AnnotationBuilder.new(@model, @options).build,
31
+ ExclusionConstraintAnnotation::AnnotationBuilder.new(@model, @options).build,
30
32
  EnumAnnotation::AnnotationBuilder.new(@model, @options).build,
31
33
  SchemaFooter.new
32
34
  ]
@@ -65,7 +67,7 @@ module AnnotateRb
65
67
  table_name = @model.table_name
66
68
  table_comment = @model.connection.try(:table_comment, @model.table_name)
67
69
  max_size = @model.max_schema_info_width
68
- database_name = @model.database_name if multi_db_environment?
70
+ database_name = @model.database_name if show_database_name?
69
71
 
70
72
  _annotation = Annotation.new(@options,
71
73
  version: version, table_name: table_name, table_comment: table_comment,
@@ -74,6 +76,11 @@ module AnnotateRb
74
76
 
75
77
  private
76
78
 
79
+ # TODO: Consolidate ignore_database_name and ignore_multi_database_name; they serve the same purpose
80
+ def show_database_name?
81
+ !@options[:ignore_database_name] && multi_db_environment?
82
+ end
83
+
77
84
  def multi_db_environment?
78
85
  return false if @options[:ignore_multi_database_name]
79
86
 
@@ -37,6 +37,7 @@ module AnnotateRb
37
37
  when Float, Integer then value.to_s
38
38
  # BigDecimals need to be output in a non-normalized form and quoted.
39
39
  when BigDecimal then value.to_s("F")
40
+ when EnumDefault then "#{quote(value.raw)}: #{quote(value.label)}"
40
41
  when String then value.inspect
41
42
  else
42
43
  value.inspect
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ module AnnotateRb
4
+ module ModelAnnotator
5
+ module ColumnAnnotation
6
+ # Pairs the raw DB default of an enum backed column with its label, so
7
+ # that annotations can show both.
8
+ EnumDefault = Struct.new(:raw, :label)
9
+ end
10
+ end
11
+ end
@@ -8,6 +8,7 @@ module AnnotateRb
8
8
  autoload :ColumnWrapper, "annotate_rb/model_annotator/column_annotation/column_wrapper"
9
9
  autoload :AnnotationBuilder, "annotate_rb/model_annotator/column_annotation/annotation_builder"
10
10
  autoload :DefaultValueBuilder, "annotate_rb/model_annotator/column_annotation/default_value_builder"
11
+ autoload :EnumDefault, "annotate_rb/model_annotator/column_annotation/enum_default"
11
12
  autoload :ColumnComponent, "annotate_rb/model_annotator/column_annotation/column_component"
12
13
  end
13
14
  end
@@ -0,0 +1,40 @@
1
+ # frozen_string_literal: true
2
+
3
+ module AnnotateRb
4
+ module ModelAnnotator
5
+ module ExclusionConstraintAnnotation
6
+ class Annotation
7
+ HEADER_TEXT = "Exclusion Constraints"
8
+
9
+ def initialize(constraints)
10
+ @constraints = constraints
11
+ end
12
+
13
+ def body
14
+ [
15
+ Components::BlankCommentLine.new,
16
+ Components::Header.new(HEADER_TEXT),
17
+ Components::BlankCommentLine.new,
18
+ *@constraints
19
+ ]
20
+ end
21
+
22
+ def to_markdown
23
+ body.map(&:to_markdown).join("\n")
24
+ end
25
+
26
+ def to_rdoc
27
+ body.map(&:to_rdoc).join("\n")
28
+ end
29
+
30
+ def to_yard
31
+ body.map(&:to_yard).join("\n")
32
+ end
33
+
34
+ def to_default
35
+ body.map(&:to_default).join("\n")
36
+ end
37
+ end
38
+ end
39
+ end
40
+ end
@@ -0,0 +1,38 @@
1
+ # frozen_string_literal: true
2
+
3
+ module AnnotateRb
4
+ module ModelAnnotator
5
+ module ExclusionConstraintAnnotation
6
+ class AnnotationBuilder
7
+ def initialize(model, options)
8
+ @model = model
9
+ @options = options
10
+ end
11
+
12
+ def build
13
+ return Components::NilComponent.new if !@options[:show_exclusion_constraints]
14
+ return Components::NilComponent.new unless @model.connection.respond_to?(:supports_exclusion_constraints?) &&
15
+ @model.connection.supports_exclusion_constraints? && @model.connection.respond_to?(:exclusion_constraints)
16
+
17
+ exclusion_constraints = @model.connection.exclusion_constraints(@model.table_name)
18
+ return Components::NilComponent.new if exclusion_constraints.empty?
19
+
20
+ max_size = exclusion_constraints.map { |exclusion_constraint| exclusion_constraint.name.size }.max + 1
21
+
22
+ constraints = exclusion_constraints.sort_by(&:name).map do |exclusion_constraint|
23
+ details = "(#{exclusion_constraint.expression})"
24
+ details += " USING #{exclusion_constraint.using}" if exclusion_constraint.using
25
+ details += " WHERE (#{exclusion_constraint.where})" if exclusion_constraint.where
26
+ if exclusion_constraint.deferrable
27
+ details += " DEFERRABLE INITIALLY #{exclusion_constraint.deferrable.to_s.upcase}"
28
+ end
29
+
30
+ ExclusionConstraintComponent.new(exclusion_constraint.name, details, max_size)
31
+ end
32
+
33
+ _annotation = Annotation.new(constraints)
34
+ end
35
+ end
36
+ end
37
+ end
38
+ end
@@ -0,0 +1,27 @@
1
+ # frozen_string_literal: true
2
+
3
+ module AnnotateRb
4
+ module ModelAnnotator
5
+ module ExclusionConstraintAnnotation
6
+ class ExclusionConstraintComponent < Components::Base
7
+ attr_reader :name, :details, :max_size
8
+
9
+ def initialize(name, details, max_size)
10
+ @name = name
11
+ @details = details
12
+ @max_size = max_size
13
+ end
14
+
15
+ def to_default
16
+ # standard:disable Lint/FormatParameterMismatch
17
+ sprintf("# %-#{max_size}.#{max_size}s %s", name, details).rstrip
18
+ # standard:enable Lint/FormatParameterMismatch
19
+ end
20
+
21
+ def to_markdown
22
+ sprintf("# * `%s`: `%s`", name, details)
23
+ end
24
+ end
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ module AnnotateRb
4
+ module ModelAnnotator
5
+ module ExclusionConstraintAnnotation
6
+ autoload :AnnotationBuilder, "annotate_rb/model_annotator/exclusion_constraint_annotation/annotation_builder"
7
+ autoload :Annotation, "annotate_rb/model_annotator/exclusion_constraint_annotation/annotation"
8
+ autoload :ExclusionConstraintComponent, "annotate_rb/model_annotator/exclusion_constraint_annotation/exclusion_constraint_component"
9
+ end
10
+ end
11
+ end
@@ -19,9 +19,19 @@ module AnnotateRb
19
19
  SCHEMA_HEADER_EXACT = [
20
20
  IndexAnnotation::Annotation::HEADER_TEXT,
21
21
  ForeignKeyAnnotation::Annotation::HEADER_TEXT,
22
- CheckConstraintAnnotation::Annotation::HEADER_TEXT
22
+ CheckConstraintAnnotation::Annotation::HEADER_TEXT,
23
+ UniqueConstraintAnnotation::Annotation::HEADER_TEXT,
24
+ ExclusionConstraintAnnotation::Annotation::HEADER_TEXT,
25
+ EnumAnnotation::Annotation::HEADER_TEXT
23
26
  ].freeze
24
27
 
28
+ # `format_markdown: true` renders each of the section headers above as
29
+ # "### <name>" instead of "<name>", so SCHEMA_HEADER_EXACT can't match it directly.
30
+ # There's no HEADER_TEXT constant for the columns table itself,
31
+ # so "Columns" is listed explicitly here.
32
+ MARKDOWN_HEADER_PREFIX = "### "
33
+ MARKDOWN_SECTION_HEADERS = (SCHEMA_HEADER_EXACT + ["Columns"]).freeze
34
+
25
35
  class MalformedAnnotation < StandardError; end
26
36
 
27
37
  class NoAnnotationFound < StandardError; end
@@ -118,7 +128,18 @@ module AnnotateRb
118
128
  ending
119
129
  end
120
130
 
121
- # Tabular rows have ≥2 leading spaces after `#`; prose has at most one.
131
+ # A line is part of the schema block if it matches one of the known
132
+ # header prefixes/exact texts, or if it looks like a tabular/list row
133
+ # of the annotation body:
134
+ #
135
+ # - Default/plain format: rows have >= 2 leading spaces after `#`
136
+ # (e.g. "# id :integer not null"); prose has at most one.
137
+ # - Markdown format (`format_markdown: true`) instead uses a single
138
+ # leading space after `#` for everything, so it needs its own
139
+ # detection: "### <name>" section headers (matched against
140
+ # MARKDOWN_SECTION_HEADERS with the prefix stripped), "|"-delimited
141
+ # table rows (header/divider/data), and "* `...`" bulleted list
142
+ # items for indexes/foreign keys/enums.
122
143
  def schema_like?(comment)
123
144
  return true if comment == DEFAULT_ANNOTATION_ENDING
124
145
 
@@ -128,6 +149,21 @@ module AnnotateRb
128
149
  return true if SCHEMA_HEADER_PREFIXES.any? { |p| text.start_with?(p) }
129
150
  return true if SCHEMA_HEADER_EXACT.include?(text)
130
151
 
152
+ # Markdown section headers, e.g. "### Columns", "### Indexes".
153
+ if text.start_with?(MARKDOWN_HEADER_PREFIX)
154
+ return MARKDOWN_SECTION_HEADERS.include?(text.delete_prefix(MARKDOWN_HEADER_PREFIX))
155
+ end
156
+
157
+ # Requires at least two | separators to identify Markdown tables.
158
+ # This prevents normal user comments containing a single | from being mistaken for a table.
159
+ return true if text.count("|") >= 2
160
+
161
+ # Markdown bulleted list items used for foreign keys/indexes/enums,
162
+ # e.g. "* `index_name`:" or the nested " * **`column`**". The
163
+ # backtick right after the bullet marker distinguishes these from
164
+ # an unrelated user comment that starts with a plain "* ".
165
+ return true if text.match?(/\A\s*\*\s+\*{0,2}`/)
166
+
131
167
  comment.match?(/\A#\s{2,}\S/)
132
168
  end
133
169
  end
@@ -31,7 +31,7 @@ module AnnotateRb
31
31
  def parse_comments
32
32
  # Adds 0-indexed line numbers
33
33
  @input.split($/).each_with_index do |line, line_no|
34
- if line.strip.starts_with?("#")
34
+ if line.strip.start_with?("#")
35
35
  @comments << [line, line_no]
36
36
  end
37
37
  end
@@ -43,10 +43,20 @@ module AnnotateRb
43
43
  begin
44
44
  parser.parse(@input)
45
45
  rescue Psych::SyntaxError => _e
46
- # "Dynamic fixtures with ERB" exist in Rails, and will cause Psych.parser to error
47
- # This is a hacky solution to get around this and still have it parse
48
- erb_yml = ERB.new(@input).result
49
- parser.parse(erb_yml)
46
+ # "Dynamic fixtures with ERB" exist in Rails and cause Psych.parser to error.
47
+ #
48
+ # We deliberately do not evaluate the ERB and read line numbers off the
49
+ # result: evaluating runs arbitrary code, and the line numbers from the
50
+ # evaluated output do not map back to the original file (ERB tags spanning
51
+ # multiple lines shift the offsets), which would place annotations inside
52
+ # an ERB tag. Instead we derive the content bounds straight from the
53
+ # original lines so annotations land around the ERB body.
54
+ #
55
+ # Only fall back for actual ERB fixtures. Genuinely malformed YAML
56
+ # (no ERB tags) should keep surfacing the parse error rather than
57
+ # being silently annotated.
58
+ raise unless erb_fixture?
59
+ return record_erb_positions
50
60
  end
51
61
 
52
62
  stream = parser.handler.root
@@ -68,6 +78,35 @@ module AnnotateRb
68
78
  @ends << [nil, stream.end_line]
69
79
  end
70
80
  end
81
+
82
+ # Locates the content bounds of an ERB fixture directly from the original
83
+ # lines, treating the ERB/YAML body as the doc. The start is the first
84
+ # non-blank, non-comment line so annotations are written above the ERB
85
+ # block (and after any leading comments), never inside a tag.
86
+ def record_erb_positions
87
+ lines = @input.split($/)
88
+ content_start = lines.index { |line| content_line?(line) }
89
+
90
+ if content_start.nil?
91
+ @starts << [nil, 0]
92
+ @ends << [nil, 0]
93
+ else
94
+ content_end = lines.rindex { |line| content_line?(line) }
95
+ @starts << [nil, content_start]
96
+ @ends << [nil, content_end + 1]
97
+ end
98
+ end
99
+
100
+ def content_line?(line)
101
+ stripped = line.strip
102
+ !stripped.empty? && !stripped.start_with?("#")
103
+ end
104
+
105
+ # True when the input contains an ERB tag, i.e. it is a dynamic fixture
106
+ # rather than plain (possibly malformed) YAML.
107
+ def erb_fixture?
108
+ @input.match?(/<%.*?%>/m)
109
+ end
71
110
  end
72
111
  end
73
112
  end
@@ -13,7 +13,7 @@ module AnnotateRb
13
13
 
14
14
  def formatted_name
15
15
  @formatted_name ||= if foreign_key.name.blank?
16
- foreign_key.column
16
+ stringified_columns
17
17
  else
18
18
  @options[:show_complete_foreign_keys] ? foreign_key.name : foreign_key.name.gsub(/(?<=^fk_rails_)[0-9a-f]{10}$/, "...")
19
19
  end
@@ -32,6 +32,12 @@ module AnnotateRb
32
32
  constraints_info = ""
33
33
  constraints_info += "ON DELETE => #{foreign_key.on_delete} " if foreign_key.on_delete
34
34
  constraints_info += "ON UPDATE => #{foreign_key.on_update} " if foreign_key.on_update
35
+ if foreign_key.respond_to?(:deferrable) && foreign_key.deferrable
36
+ # Rails 7.0's extract_foreign_key_deferrable returns `true` for the initially-immediate case;
37
+ # 7.1+ normalized this to `:immediate`. Map `true` back to IMMEDIATE so we don't emit "INITIALLY TRUE".
38
+ initially = (foreign_key.deferrable == true) ? "IMMEDIATE" : foreign_key.deferrable.to_s.upcase
39
+ constraints_info += "DEFERRABLE INITIALLY #{initially} "
40
+ end
35
41
  constraints_info.strip
36
42
  end
37
43
  end
@@ -15,6 +15,9 @@ module AnnotateRb
15
15
  indexes = @model.retrieve_indexes_from_table
16
16
  return Components::NilComponent.new if indexes.empty?
17
17
 
18
+ indexes = reject_constraint_backed_indexes(indexes)
19
+ return Components::NilComponent.new if indexes.empty?
20
+
18
21
  max_size = indexes.map { |index| index.name.size }.max + 1
19
22
 
20
23
  indexes = indexes.sort_by(&:name).map do |index|
@@ -23,6 +26,32 @@ module AnnotateRb
23
26
 
24
27
  _annotation = Annotation.new(indexes)
25
28
  end
29
+
30
+ private
31
+
32
+ # Mirrors ActiveRecord's schema_dumper#indexes_in_create: PostgreSQL's
33
+ # unique and exclusion constraints are backed by indexes with the same
34
+ # name, so those show up in `connection.indexes` too. Drop them here so
35
+ # they only appear under their dedicated sections.
36
+ def reject_constraint_backed_indexes(indexes)
37
+ connection = @model.connection
38
+
39
+ if connection.respond_to?(:supports_exclusion_constraints?) &&
40
+ connection.supports_exclusion_constraints? &&
41
+ connection.respond_to?(:exclusion_constraints)
42
+ excl_names = connection.exclusion_constraints(@model.table_name).map(&:name)
43
+ indexes = indexes.reject { |index| excl_names.include?(index.name) }
44
+ end
45
+
46
+ if connection.respond_to?(:supports_unique_constraints?) &&
47
+ connection.supports_unique_constraints? &&
48
+ connection.respond_to?(:unique_constraints)
49
+ unique_names = connection.unique_constraints(@model.table_name).map(&:name)
50
+ indexes = indexes.reject { |index| unique_names.include?(index.name) }
51
+ end
52
+
53
+ indexes
54
+ end
26
55
  end
27
56
  end
28
57
  end
@@ -136,13 +136,23 @@ module AnnotateRb
136
136
 
137
137
  def columns_info
138
138
  Array(index.columns).map do |col|
139
- if index.try(:orders) && index.orders[col.to_s]
140
- "#{col} #{index.orders[col.to_s].upcase}"
141
- else
142
- col.to_s.gsub("\r", '\r').gsub("\n", '\n')
143
- end
139
+ column = col.to_s.gsub("\r", '\r').gsub("\n", '\n')
140
+ opclass = column_option(:opclasses, col)
141
+ order = column_option(:orders, col)
142
+
143
+ [column, opclass, order&.upcase].compact.join(" ")
144
144
  end
145
145
  end
146
+
147
+ # ActiveRecord condenses per column index options (`opclasses`, `orders`)
148
+ # into a single value when every column shares the same one, so the
149
+ # option can either be a Hash keyed by column or a bare value.
150
+ def column_option(option, col)
151
+ value = index.try(option)
152
+ value = value[col.to_s] if value.is_a?(Hash)
153
+
154
+ value.presence&.to_s
155
+ end
146
156
  end
147
157
  end
148
158
  end
@@ -68,8 +68,23 @@ module AnnotateRb
68
68
  connection.table_comment(@klass.table_name).present?
69
69
  end
70
70
 
71
+ # Returns column defaults for annotations.
72
+ #
73
+ # `Model#column_defaults` reflects `attribute :foo, default: X` overrides,
74
+ # which would incorrectly show the Ruby-side default in annotations
75
+ # instead of the DB schema default. To preserve model-level decorations
76
+ # such as enum labels or `TimeZoneConverter` on datetime columns, we start
77
+ # from `column_defaults` and only substitute the DB schema value for
78
+ # attributes whose raw default was replaced.
71
79
  def column_defaults
72
- @klass.column_defaults
80
+ @column_defaults ||= @klass.column_defaults.each_with_object({}) do |(name, value), result|
81
+ column = @klass.columns_hash[name]
82
+ result[name] = if attribute_default_overridden?(name, column)
83
+ schema_default_for(column)
84
+ else
85
+ enum_default(name, column, value)
86
+ end
87
+ end
73
88
  end
74
89
 
75
90
  # Add columns managed by the globalize gem if this gem is being used.
@@ -263,6 +278,53 @@ module AnnotateRb
263
278
 
264
279
  @options.get_state(cache_key)
265
280
  end
281
+
282
+ private
283
+
284
+ # `attribute :foo, default: X` replaces the raw default Rails read from
285
+ # the DB column, so comparing the two raw values detects the override.
286
+ # Comparing cast values instead would misreport decorated attribute types
287
+ # (an enum casts the DB default `0` into its label) as overrides.
288
+ def attribute_default_overridden?(name, column)
289
+ return false if column.nil?
290
+
291
+ @klass._default_attributes[name].value_before_type_cast != column.default
292
+ end
293
+
294
+ # An enum attribute type casts the raw DB default into its label, which is
295
+ # what `column_defaults` reports and what annotations have historically
296
+ # shown. `enum_default_format` picks between that label, the raw value,
297
+ # and both.
298
+ def enum_default(name, column, label)
299
+ return label unless @klass.defined_enums.key?(name)
300
+
301
+ raw = schema_default_for(column)
302
+ return label if raw == label
303
+
304
+ case @options[:enum_default_format]
305
+ when :raw then raw
306
+ when :both then ColumnAnnotation::EnumDefault.new(raw, label)
307
+ else label
308
+ end
309
+ end
310
+
311
+ def schema_default_for(column)
312
+ return nil if column.nil? || column.default.nil? || column.default_function
313
+ cast_type_for(column).deserialize(column.default)
314
+ end
315
+
316
+ # Rails post-8.1 exposes `Column#cast_type` directly; Rails 8.1 introduced
317
+ # the transitional `Column#fetch_cast_type(connection)`; older versions
318
+ # required `connection.lookup_cast_type_from_column(column)`.
319
+ def cast_type_for(column)
320
+ if column.respond_to?(:cast_type)
321
+ column.cast_type
322
+ elsif column.respond_to?(:fetch_cast_type)
323
+ column.fetch_cast_type(connection)
324
+ else
325
+ connection.lookup_cast_type_from_column(column)
326
+ end
327
+ end
266
328
  end
267
329
  end
268
330
  end
@@ -43,7 +43,9 @@ module AnnotateRb
43
43
 
44
44
  klass.reset_column_information
45
45
  model_name = klass.name.underscore
46
- table_name = klass.table_name
46
+ # Match annotation behavior so a secondary model with a duplicate table
47
+ # name cannot remove annotations from the primary database's fixture.
48
+ table_name = klass.table_name if klass.connection_specification_name == ActiveRecord::Base.name
47
49
 
48
50
  model_instruction = SingleFileRemoveAnnotationInstruction.new(file, @options)
49
51
  instructions << model_instruction
@@ -88,8 +88,33 @@ module AnnotateRb
88
88
 
89
89
  patterns
90
90
  .map { |f| FileNameResolver.call(f, @model_name, @table_name) }
91
- .map { |f| Dir.glob(f) }
92
- .flatten
91
+ .flat_map { |f| Dir.glob(f) }
92
+ .select { |f| owning_root_dir(f) == model_root_dir }
93
+ end
94
+
95
+ def model_root_dir
96
+ return @model_root_dir if defined?(@model_root_dir)
97
+
98
+ @model_root_dir = owning_root_dir(@file)
99
+ end
100
+
101
+ # Patterns are expanded for every `root_dir` and resolved by model name alone, so models sharing a
102
+ # file basename across root directories (e.g. packwerk packs) glob the same related files. Root
103
+ # directories can be nested, so a file belongs to the most specific one containing it, and only
104
+ # models from that same root directory may annotate it. Returns nil for the project root.
105
+ def owning_root_dir(file)
106
+ expanded_file = File.expand_path(file)
107
+
108
+ expanded_root_dirs
109
+ .select { |dir| expanded_file.start_with?("#{dir}/") }
110
+ .max_by(&:length)
111
+ end
112
+
113
+ def expanded_root_dirs
114
+ @expanded_root_dirs ||= Array(@options[:root_dir])
115
+ .reject { |root_dir| root_dir.to_s.empty? }
116
+ .flat_map { |root_dir| Dir.glob(root_dir) }
117
+ .map { |root_dir| File.expand_path(root_dir) }
93
118
  end
94
119
 
95
120
  def add_related_test_files
@@ -18,7 +18,7 @@ module AnnotateRb
18
18
  parsed_file = FileParser::ParsedFile.new(old_content, "", parser_klass, options).parse
19
19
  rescue FileParser::AnnotationFinder::MalformedAnnotation => e
20
20
  warn "Unable to process #{file_name}: #{e.message}"
21
- warn "\t" + e.backtrace.join("\n\t") if @options[:trace]
21
+ warn "\t" + e.backtrace.join("\n\t") if options[:trace]
22
22
  return false
23
23
  end
24
24
 
@@ -31,7 +31,7 @@ module AnnotateRb
31
31
  parsed_file = FileParser::ParsedFile.new(old_content, annotation, parser_klass, options, model_class_name: model_class_name).parse
32
32
  rescue FileParser::AnnotationFinder::MalformedAnnotation => e
33
33
  warn "Unable to process #{file_name}: #{e.message}"
34
- warn "\t" + e.backtrace.join("\n\t") if @options[:trace]
34
+ warn "\t" + e.backtrace.join("\n\t") if options[:trace]
35
35
  return false
36
36
  end
37
37
 
@@ -0,0 +1,40 @@
1
+ # frozen_string_literal: true
2
+
3
+ module AnnotateRb
4
+ module ModelAnnotator
5
+ module UniqueConstraintAnnotation
6
+ class Annotation
7
+ HEADER_TEXT = "Unique Constraints"
8
+
9
+ def initialize(constraints)
10
+ @constraints = constraints
11
+ end
12
+
13
+ def body
14
+ [
15
+ Components::BlankCommentLine.new,
16
+ Components::Header.new(HEADER_TEXT),
17
+ Components::BlankCommentLine.new,
18
+ *@constraints
19
+ ]
20
+ end
21
+
22
+ def to_markdown
23
+ body.map(&:to_markdown).join("\n")
24
+ end
25
+
26
+ def to_rdoc
27
+ body.map(&:to_rdoc).join("\n")
28
+ end
29
+
30
+ def to_yard
31
+ body.map(&:to_yard).join("\n")
32
+ end
33
+
34
+ def to_default
35
+ body.map(&:to_default).join("\n")
36
+ end
37
+ end
38
+ end
39
+ end
40
+ end
@@ -0,0 +1,37 @@
1
+ # frozen_string_literal: true
2
+
3
+ module AnnotateRb
4
+ module ModelAnnotator
5
+ module UniqueConstraintAnnotation
6
+ class AnnotationBuilder
7
+ def initialize(model, options)
8
+ @model = model
9
+ @options = options
10
+ end
11
+
12
+ def build
13
+ return Components::NilComponent.new if !@options[:show_unique_constraints]
14
+ return Components::NilComponent.new unless @model.connection.respond_to?(:supports_unique_constraints?) &&
15
+ @model.connection.supports_unique_constraints? && @model.connection.respond_to?(:unique_constraints)
16
+
17
+ unique_constraints = @model.connection.unique_constraints(@model.table_name)
18
+ return Components::NilComponent.new if unique_constraints.empty?
19
+
20
+ max_size = unique_constraints.map { |unique_constraint| unique_constraint.name.size }.max + 1
21
+
22
+ constraints = unique_constraints.sort_by(&:name).map do |unique_constraint|
23
+ columns = Array(unique_constraint.column)
24
+ details = "(#{columns.join(", ")})"
25
+ if unique_constraint.deferrable
26
+ details += " DEFERRABLE INITIALLY #{unique_constraint.deferrable.to_s.upcase}"
27
+ end
28
+
29
+ UniqueConstraintComponent.new(unique_constraint.name, details, max_size)
30
+ end
31
+
32
+ _annotation = Annotation.new(constraints)
33
+ end
34
+ end
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,27 @@
1
+ # frozen_string_literal: true
2
+
3
+ module AnnotateRb
4
+ module ModelAnnotator
5
+ module UniqueConstraintAnnotation
6
+ class UniqueConstraintComponent < Components::Base
7
+ attr_reader :name, :details, :max_size
8
+
9
+ def initialize(name, details, max_size)
10
+ @name = name
11
+ @details = details
12
+ @max_size = max_size
13
+ end
14
+
15
+ def to_default
16
+ # standard:disable Lint/FormatParameterMismatch
17
+ sprintf("# %-#{max_size}.#{max_size}s %s", name, details).rstrip
18
+ # standard:enable Lint/FormatParameterMismatch
19
+ end
20
+
21
+ def to_markdown
22
+ sprintf("# * `%s`: `%s`", name, details)
23
+ end
24
+ end
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ module AnnotateRb
4
+ module ModelAnnotator
5
+ module UniqueConstraintAnnotation
6
+ autoload :AnnotationBuilder, "annotate_rb/model_annotator/unique_constraint_annotation/annotation_builder"
7
+ autoload :Annotation, "annotate_rb/model_annotator/unique_constraint_annotation/annotation"
8
+ autoload :UniqueConstraintComponent, "annotate_rb/model_annotator/unique_constraint_annotation/unique_constraint_component"
9
+ end
10
+ end
11
+ end
@@ -27,6 +27,8 @@ module AnnotateRb
27
27
  autoload :FileParser, "annotate_rb/model_annotator/file_parser"
28
28
  autoload :ZeitwerkClassGetter, "annotate_rb/model_annotator/zeitwerk_class_getter"
29
29
  autoload :CheckConstraintAnnotation, "annotate_rb/model_annotator/check_constraint_annotation"
30
+ autoload :UniqueConstraintAnnotation, "annotate_rb/model_annotator/unique_constraint_annotation"
31
+ autoload :ExclusionConstraintAnnotation, "annotate_rb/model_annotator/exclusion_constraint_annotation"
30
32
  autoload :EnumAnnotation, "annotate_rb/model_annotator/enum_annotation"
31
33
  autoload :FileToParserMapper, "annotate_rb/model_annotator/file_to_parser_mapper"
32
34
  autoload :Components, "annotate_rb/model_annotator/components"
@@ -42,11 +42,14 @@ module AnnotateRb
42
42
  format_yard: false, # ModelAnnotator
43
43
  frozen: false, # ModelAnnotator, but should be used by both
44
44
  grouped_polymorphic: false, # ModelAnnotator
45
+ ignore_database_name: false, # ModelAnnotator
45
46
  ignore_model_sub_dir: false, # ModelAnnotator
46
47
  ignore_unknown_models: false, # ModelAnnotator
47
48
  include_version: false, # ModelAnnotator
48
49
  show_complete_foreign_keys: false, # ModelAnnotator
49
50
  show_check_constraints: false, # ModelAnnotator
51
+ show_unique_constraints: false, # ModelAnnotator
52
+ show_exclusion_constraints: false, # ModelAnnotator
50
53
  show_enums: false, # ModelAnnotator
51
54
  show_foreign_keys: true, # ModelAnnotator
52
55
  show_indexes: true, # ModelAnnotator
@@ -60,7 +63,8 @@ module AnnotateRb
60
63
  with_comment: true, # ModelAnnotator
61
64
  with_column_comments: nil, # ModelAnnotator
62
65
  with_table_comments: nil, # ModelAnnotator
63
- position_of_column_comment: :with_name # ModelAnnotator
66
+ position_of_column_comment: :with_name, # ModelAnnotator
67
+ enum_default_format: :label # ModelAnnotator
64
68
  }.freeze
65
69
 
66
70
  OTHER_OPTIONS = {
@@ -81,7 +85,6 @@ module AnnotateRb
81
85
  ignore_multi_database_name: false, # ModelAnnotator
82
86
  ignore_routes: nil, # RouteAnnotator
83
87
  models: true, # Core
84
- routes: false, # Core
85
88
  skip_on_db_migrate: false, # Core
86
89
  auto_annotate_routes_after_migrate: false, # Core
87
90
  target_action: :do_annotations, # Core; Possible values: :do_annotations, :remove_annotations
@@ -118,10 +121,13 @@ module AnnotateRb
118
121
  :format_yard,
119
122
  :frozen,
120
123
  :grouped_polymorphic,
124
+ :ignore_database_name,
121
125
  :ignore_model_sub_dir,
122
126
  :ignore_unknown_models,
123
127
  :include_version,
124
128
  :show_check_constraints,
129
+ :show_unique_constraints,
130
+ :show_exclusion_constraints,
125
131
  :show_enums,
126
132
  :show_complete_foreign_keys,
127
133
  :show_foreign_keys,
@@ -135,7 +141,8 @@ module AnnotateRb
135
141
  :with_comment,
136
142
  :with_column_comments,
137
143
  :with_table_comments,
138
- :position_of_column_comment
144
+ :position_of_column_comment,
145
+ :enum_default_format
139
146
  ].freeze
140
147
 
141
148
  OTHER_OPTION_KEYS = [
@@ -149,7 +156,6 @@ module AnnotateRb
149
156
  :ignore_routes,
150
157
  :ignore_multi_database_name,
151
158
  :models,
152
- :routes,
153
159
  :skip_on_db_migrate,
154
160
  :auto_annotate_routes_after_migrate,
155
161
  :target_action,
@@ -218,6 +224,7 @@ module AnnotateRb
218
224
  @options[:with_column_comments] = @options[:with_comment] if @options[:with_column_comments].nil?
219
225
  @options[:with_table_comments] = @options[:with_comment] if @options[:with_table_comments].nil?
220
226
  @options[:position_of_column_comment] = @options[:position_of_column_comment].to_sym
227
+ @options[:enum_default_format] = @options[:enum_default_format].to_sym
221
228
 
222
229
  self
223
230
  end
@@ -207,6 +207,16 @@ module AnnotateRb
207
207
  @options[:show_check_constraints] = true
208
208
  end
209
209
 
210
+ option_parser.on("--show-unique-constraints",
211
+ "List the table's unique constraints in the annotation") do
212
+ @options[:show_unique_constraints] = true
213
+ end
214
+
215
+ option_parser.on("--show-exclusion-constraints",
216
+ "List the table's exclusion constraints in the annotation") do
217
+ @options[:show_exclusion_constraints] = true
218
+ end
219
+
210
220
  option_parser.on("--hide-limit-column-types VALUES",
211
221
  "don't show limit for given column types, separated by commas (i.e., `integer,boolean,text`)") do |values|
212
222
  @options[:hide_limit_column_types] = values.to_s
@@ -222,6 +232,11 @@ module AnnotateRb
222
232
  @options[:ignore_unknown_models] = true
223
233
  end
224
234
 
235
+ option_parser.on("--ignore-database-name",
236
+ "don't include the database name in the annotation") do
237
+ @options[:ignore_database_name] = true
238
+ end
239
+
225
240
  option_parser.on("-I",
226
241
  "--ignore-columns REGEX",
227
242
  "don't annotate columns that match a given REGEX (i.e., `annotate -I '^(id|updated_at|created_at)'`") do |regex|
@@ -234,7 +249,7 @@ module AnnotateRb
234
249
  end
235
250
 
236
251
  option_parser.on("--without-comment",
237
- "include database comments in model annotations") do
252
+ "exclude database comments in model annotations") do
238
253
  @options[:with_comment] = false
239
254
  end
240
255
 
@@ -253,6 +268,11 @@ module AnnotateRb
253
268
  @options[:position_of_column_comment] = value.to_sym
254
269
  end
255
270
 
271
+ option_parser.on("--enum-default-format [label|raw|both]",
272
+ "set how defaults of enum backed columns are shown") do |value|
273
+ @options[:enum_default_format] = value.to_sym
274
+ end
275
+
256
276
  option_parser.on("--with-table-comments",
257
277
  "include table comments in model annotations") do
258
278
  @options[:with_table_comments] = true
@@ -46,8 +46,6 @@ module AnnotateRb
46
46
  raise "Didn't specify a command" unless @options[:command]
47
47
 
48
48
  @options[:command].call(@options)
49
-
50
- # TODO
51
49
  end
52
50
  end
53
51
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: annotaterb
3
3
  version: !ruby/object:Gem::Version
4
- version: 4.23.0
4
+ version: 4.25.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Andrew W. Lee
@@ -88,12 +88,17 @@ files:
88
88
  - lib/annotate_rb/model_annotator/column_annotation/column_component.rb
89
89
  - lib/annotate_rb/model_annotator/column_annotation/column_wrapper.rb
90
90
  - lib/annotate_rb/model_annotator/column_annotation/default_value_builder.rb
91
+ - lib/annotate_rb/model_annotator/column_annotation/enum_default.rb
91
92
  - lib/annotate_rb/model_annotator/column_annotation/type_builder.rb
92
93
  - lib/annotate_rb/model_annotator/components.rb
93
94
  - lib/annotate_rb/model_annotator/enum_annotation.rb
94
95
  - lib/annotate_rb/model_annotator/enum_annotation/annotation.rb
95
96
  - lib/annotate_rb/model_annotator/enum_annotation/annotation_builder.rb
96
97
  - lib/annotate_rb/model_annotator/enum_annotation/enum_component.rb
98
+ - lib/annotate_rb/model_annotator/exclusion_constraint_annotation.rb
99
+ - lib/annotate_rb/model_annotator/exclusion_constraint_annotation/annotation.rb
100
+ - lib/annotate_rb/model_annotator/exclusion_constraint_annotation/annotation_builder.rb
101
+ - lib/annotate_rb/model_annotator/exclusion_constraint_annotation/exclusion_constraint_component.rb
97
102
  - lib/annotate_rb/model_annotator/file_name_resolver.rb
98
103
  - lib/annotate_rb/model_annotator/file_parser.rb
99
104
  - lib/annotate_rb/model_annotator/file_parser/annotation_finder.rb
@@ -124,6 +129,10 @@ files:
124
129
  - lib/annotate_rb/model_annotator/single_file_annotator.rb
125
130
  - lib/annotate_rb/model_annotator/single_file_annotator_instruction.rb
126
131
  - lib/annotate_rb/model_annotator/single_file_remove_annotation_instruction.rb
132
+ - lib/annotate_rb/model_annotator/unique_constraint_annotation.rb
133
+ - lib/annotate_rb/model_annotator/unique_constraint_annotation/annotation.rb
134
+ - lib/annotate_rb/model_annotator/unique_constraint_annotation/annotation_builder.rb
135
+ - lib/annotate_rb/model_annotator/unique_constraint_annotation/unique_constraint_component.rb
127
136
  - lib/annotate_rb/model_annotator/zeitwerk_class_getter.rb
128
137
  - lib/annotate_rb/options.rb
129
138
  - lib/annotate_rb/parser.rb