thecore_generators 3.1.0 → 3.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/README.md +240 -9
- data/lib/generators/thecore/action_companion.rb +116 -0
- data/lib/generators/thecore/association_wiring.rb +217 -0
- data/lib/generators/thecore/atom_aware.rb +14 -3
- data/lib/generators/thecore/companion_files.rb +142 -0
- data/lib/generators/thecore/member_action/member_action_generator.rb +61 -0
- data/lib/generators/thecore/member_action/templates/action.html.erb.tt +16 -0
- data/lib/generators/thecore/member_action/templates/action.js.tt +44 -0
- data/lib/generators/thecore/member_action/templates/action.rb.tt +25 -0
- data/lib/generators/thecore/member_action/templates/action.scss.tt +38 -0
- data/lib/generators/thecore/migration/migration_generator.rb +18 -7
- data/lib/generators/thecore/model/model_generator.rb +55 -15
- data/lib/generators/thecore/root_action/root_action_generator.rb +80 -0
- data/lib/generators/thecore/root_action/templates/action.html.erb.tt +13 -0
- data/lib/generators/thecore/root_action/templates/action.js.tt +42 -0
- data/lib/generators/thecore/root_action/templates/action.rb.tt +33 -0
- data/lib/generators/thecore/root_action/templates/action.scss.tt +38 -0
- data/lib/generators/thecore/workspace_context.rb +48 -0
- data/lib/tasks/thecore_generators_tasks.rake +33 -0
- data/lib/thecore_generators/check_practices.rb +392 -0
- data/lib/thecore_generators/railtie.rb +4 -0
- data/lib/thecore_generators/version.rb +1 -1
- metadata +16 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: b52de470428f2cecb91388721b140a88d6cdb0a1e0ea7b3ce07b0b855e17fbaa
|
|
4
|
+
data.tar.gz: 5a6147ac698f2820027e493bd8bdee96c13f39867dd50f6b472a495f5f01b829
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 52374b36988c76209702afb27c59e32ac95e05c53ca805914257264bb3eaae31ee06fa3329faae3a6da5c25742d31e136f470a22cf1897cdbd9a17caad010f97
|
|
7
|
+
data.tar.gz: 75083924ea8bdc22bf39d5dbea930da5ec5eb37d736107b43037e235d40a4430b0ae963a24133b9789631135de83b51cdfb1afd82515c4d2f0c69ddb43bb30a2
|
data/README.md
CHANGED
|
@@ -12,10 +12,16 @@ own `thecore:*`-namespaced generators or an application template. See
|
|
|
12
12
|
in the thecore repo for the full design.
|
|
13
13
|
|
|
14
14
|
**Status:** Model + Migration generator hook (Phase 1 of
|
|
15
|
-
[ADR 0002](https://github.com/gabrieletassoni/thecore/blob/release/3/docs/adr/0002-thecore-generators-gem-and-generator-hook-mechanism.md))
|
|
15
|
+
[ADR 0002](https://github.com/gabrieletassoni/thecore/blob/release/3/docs/adr/0002-thecore-generators-gem-and-generator-hook-mechanism.md)),
|
|
16
|
+
plus default-concern removal ([ADR 0001](https://github.com/gabrieletassoni/thecore/blob/release/3/docs/adr/0001-application-record-defaults-over-generated-concerns.md))
|
|
17
|
+
and migration-driven inverse-association wiring
|
|
18
|
+
([ADR 0003](https://github.com/gabrieletassoni/thecore/blob/release/3/docs/adr/0003-migration-driven-inverse-association-wiring.md)).
|
|
16
19
|
`ThecoreGenerators::Railtie` registers `config.app_generators.orm :thecore, migration:
|
|
17
20
|
true, timestamps: true`, so plain `rails generate model`/`rails generate migration`
|
|
18
|
-
transparently apply thecore's scaffolding conventions — no new command vocabulary.
|
|
21
|
+
transparently apply thecore's scaffolding conventions — no new command vocabulary. **Phase 2
|
|
22
|
+
(check_practices + Root/Member Action generators) is complete** as of this release: `rails
|
|
23
|
+
generate thecore:root_action`, `rails generate thecore:member_action`, and `rails
|
|
24
|
+
thecore:check_practices` (Scaffold Files + Models + Actions, `--fix` included) all ship.
|
|
19
25
|
|
|
20
26
|
### What `rails generate model`/`rails generate migration` do now
|
|
21
27
|
|
|
@@ -26,13 +32,23 @@ transparently apply thecore's scaffolding conventions — no new command vocabul
|
|
|
26
32
|
`app/models`/`db/migrate`/`test` instead of the host app's. Pass `--atom=NAME` to
|
|
27
33
|
override detection explicitly (works independent of `cwd`, e.g. from CI or the host-app
|
|
28
34
|
root).
|
|
29
|
-
- **
|
|
30
|
-
are generated
|
|
31
|
-
`addModel.js` templates do today.
|
|
32
|
-
- **No `Endpoints::ModelName` by default** (per
|
|
35
|
+
- **No concern files by default.** `Api::ModelName`/`RailsAdmin::ModelName` concern files
|
|
36
|
+
are **not** generated (per
|
|
33
37
|
[ADR 0001](https://github.com/gabrieletassoni/thecore/blob/release/3/docs/adr/0001-application-record-defaults-over-generated-concerns.md)) —
|
|
34
|
-
|
|
35
|
-
|
|
38
|
+
the no-customization case relies entirely on the default `json_attrs`/`navigation_label`/
|
|
39
|
+
`navigation_icon` behavior that `model_driven_api` and `thecore_ui_rails_admin` `include`
|
|
40
|
+
into every `ApplicationRecord` subclass automatically
|
|
41
|
+
(`ThecoreBackendCommons::DefaultModuleRegistry`). Pass `--with-api-concern` and/or
|
|
42
|
+
`--with-admin-concern` to scaffold a starter concern file — identical in shape to what
|
|
43
|
+
this generator produced before this default changed — for the case where customization
|
|
44
|
+
is already known to be needed at generation time:
|
|
45
|
+
```bash
|
|
46
|
+
rails generate model Foo name:string --with-api-concern --with-admin-concern
|
|
47
|
+
```
|
|
48
|
+
See "Adding a concern by hand" below for the (more common) case of realizing
|
|
49
|
+
customization is needed *after* the model already exists.
|
|
50
|
+
- **No `Endpoints::ModelName` by default** (per ADR 0001) — add one by hand, following the
|
|
51
|
+
`after_initialize` + `class_eval` pattern, only when a real custom action is needed.
|
|
36
52
|
- **Test file generation is never suppressed** — a real Minitest file is generated, same
|
|
37
53
|
as Rails' own `active_record:model` default.
|
|
38
54
|
- **`rails generate active_record:model`/`active_record:migration` still work directly**
|
|
@@ -40,9 +56,202 @@ transparently apply thecore's scaffolding conventions — no new command vocabul
|
|
|
40
56
|
|
|
41
57
|
Both `Thecore::Generators::ModelGenerator` and `MigrationGenerator` wrap (not reimplement)
|
|
42
58
|
`ActiveRecord::Generators::ModelGenerator`/`MigrationGenerator` — all attribute parsing and
|
|
43
|
-
template content is inherited as-is; only file placement and the two
|
|
59
|
+
template content is inherited as-is; only file placement and the two opt-in concerns are
|
|
44
60
|
added on top.
|
|
45
61
|
|
|
62
|
+
### Adding a concern by hand
|
|
63
|
+
|
|
64
|
+
The common case is not knowing at `rails generate model` time that a model will need
|
|
65
|
+
custom API serialization or RailsAdmin configuration — that need usually surfaces later.
|
|
66
|
+
Since neither concern is generated by default, add the missing one directly instead of
|
|
67
|
+
regenerating the model:
|
|
68
|
+
|
|
69
|
+
**`Api::ModelName`** (custom `json_attrs`) — create `app/models/concerns/api/model_name.rb`:
|
|
70
|
+
```ruby
|
|
71
|
+
module Api::ModelName
|
|
72
|
+
extend ActiveSupport::Concern
|
|
73
|
+
|
|
74
|
+
included do
|
|
75
|
+
cattr_accessor :json_attrs
|
|
76
|
+
self.json_attrs = ::ModelDrivenApi.smart_merge(json_attrs || {}), { only: [:id, :name] }
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
```
|
|
80
|
+
then `include Api::ModelName` in the model. Because the default module (`model_driven_api`'s
|
|
81
|
+
`ModelDrivenApiDefaultJsonAttrs`) is already `include`d by the time the model class body
|
|
82
|
+
runs, `::ModelDrivenApi.smart_merge(json_attrs || {}, ...)` composes on top of it rather
|
|
83
|
+
than starting from nothing — the same pattern the opt-in `--with-api-concern` template
|
|
84
|
+
below uses.
|
|
85
|
+
|
|
86
|
+
**`RailsAdmin::ModelName`** (custom admin config) — create
|
|
87
|
+
`app/models/concerns/rails_admin/model_name.rb`:
|
|
88
|
+
```ruby
|
|
89
|
+
module RailsAdmin::ModelName
|
|
90
|
+
extend ActiveSupport::Concern
|
|
91
|
+
|
|
92
|
+
included do
|
|
93
|
+
rails_admin do
|
|
94
|
+
navigation_label I18n.t('admin.registries.label')
|
|
95
|
+
navigation_icon 'fa fa-file' # see https://fontawesome.com/v5/search
|
|
96
|
+
configure :some_field do
|
|
97
|
+
hide
|
|
98
|
+
end
|
|
99
|
+
end
|
|
100
|
+
end
|
|
101
|
+
end
|
|
102
|
+
```
|
|
103
|
+
then `include RailsAdmin::ModelName` in the model. RailsAdmin evaluates same-origin
|
|
104
|
+
`rails_admin do ... end` blocks in registration order and later calls win on settings they
|
|
105
|
+
touch (`navigation_label`/`navigation_icon` are last-write-wins setters) — so this explicit
|
|
106
|
+
block, `include`d after the default from the class body, overrides the default's
|
|
107
|
+
`navigation_label`/`navigation_icon` while the default itself keeps applying to every other
|
|
108
|
+
model that has no concern of its own.
|
|
109
|
+
|
|
110
|
+
Either concern can be added independently — a model doesn't need both just because it
|
|
111
|
+
needs one.
|
|
112
|
+
|
|
113
|
+
### Inverse association wiring for `references` columns
|
|
114
|
+
|
|
115
|
+
Rails' own generators only ever wire the owning (`belongs_to`) side of a `references`/
|
|
116
|
+
`add_reference` column — the inverse `has_many`/`has_one` side has always been a manual
|
|
117
|
+
follow-up. `rails generate model`/`migration` now write that missing inverse side
|
|
118
|
+
automatically into the *target* model's own canonical concern
|
|
119
|
+
(`config/initializers/concern_<target_model>.rb`, wired up via
|
|
120
|
+
`config/initializers/after_initialize.rb`):
|
|
121
|
+
|
|
122
|
+
```bash
|
|
123
|
+
rails generate migration AddPostRefToComments post:references
|
|
124
|
+
# → prompts: "Inverse association on Post for this reference (has_many/has_one/skip)?"
|
|
125
|
+
# → writes `has_many :comments` into config/initializers/concern_post.rb
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
With a real terminal attached you're prompted for cardinality (`has_many` default, `has_one`,
|
|
129
|
+
or `skip`); pass `--non-interactive` (always used by `addModel`/`addMigration` in the VS Code
|
|
130
|
+
extension, and recommended for any other scripted/CI invocation) to skip the prompt and default
|
|
131
|
+
straight to `has_many`. Re-running the generator against the same target model never duplicates
|
|
132
|
+
an already-written association line, and a later, different reference onto the same target
|
|
133
|
+
appends into the same existing concern file. When the target model lives in a different ATOM
|
|
134
|
+
than the one being generated into, the concern is still written into the *invoking* app/ATOM
|
|
135
|
+
(never the target's own) and the generator logs — but never edits — the gemspec/Gemfile
|
|
136
|
+
dependency line a human needs to add so that include actually resolves. Full mechanics in
|
|
137
|
+
`CLAUDE.md`.
|
|
138
|
+
|
|
139
|
+
### `rails generate thecore:root_action NAME`
|
|
140
|
+
|
|
141
|
+
A Ruby port of `thecore_code_extension`'s `addRootAction.js` — produces the same end result
|
|
142
|
+
from a terminal, with the same ATOM-aware placement `rails generate model`/`migration` use:
|
|
143
|
+
|
|
144
|
+
```bash
|
|
145
|
+
rails generate thecore:root_action my_action
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
This creates:
|
|
149
|
+
|
|
150
|
+
- The RailsAdmin action config file — `lib/root_actions/my_action.rb` in ATOM context,
|
|
151
|
+
`config/root_actions/my_action.rb` in host-app context (main-app actions never live under
|
|
152
|
+
`lib/`, for Zeitwerk autoload safety).
|
|
153
|
+
- Its view/JS/SCSS companions: `app/views/rails_admin/main/my_action.html.erb`,
|
|
154
|
+
`app/assets/javascripts/rails_admin/actions/my_action.js`,
|
|
155
|
+
`app/assets/stylesheets/rails_admin/actions/my_action.scss`.
|
|
156
|
+
- A `require` line inserted into `config/initializers/after_initialize.rb` (created from a
|
|
157
|
+
skeleton if absent) — `require 'root_actions/my_action'` in ATOM context, a full
|
|
158
|
+
`Rails.root.join(...)` require in host-app context (`config/` isn't on the load path).
|
|
159
|
+
- A precompile line inserted into `config/initializers/assets.rb` (created from a skeleton if
|
|
160
|
+
absent).
|
|
161
|
+
- An `admin.actions.my_action` locale entry (`menu`/`title`/`breadcrumb`, all set to the
|
|
162
|
+
title-cased action name) written into **every** `*.yml` file already present under
|
|
163
|
+
`config/locales` — `en.yml`/`it.yml` are created first only when the directory has none yet.
|
|
164
|
+
|
|
165
|
+
`NAME` must be snake_case (lowercase letters, digits, underscores). `--atom=NAME` overrides
|
|
166
|
+
placement the same way it does for `rails generate model`/`migration`. Re-running the
|
|
167
|
+
generator against the same action name never duplicates the require line, the precompile
|
|
168
|
+
line, or a locale entry.
|
|
169
|
+
|
|
170
|
+
The reusable pieces behind this (`Thecore::Generators::CompanionFiles`,
|
|
171
|
+
`Thecore::Generators::ActionCompanion`) are shared with `thecore:member_action` (below), and
|
|
172
|
+
`check_practices --fix` (further below) delegates straight to both generators' own template
|
|
173
|
+
rendering rather than reimplementing it.
|
|
174
|
+
|
|
175
|
+
### `rails generate thecore:member_action NAME`
|
|
176
|
+
|
|
177
|
+
The Member Action counterpart to `thecore:root_action` above — a Ruby port of
|
|
178
|
+
`thecore_code_extension`'s `addMemberAction.js`, sharing everything about placement, the
|
|
179
|
+
after_initialize.rb/assets.rb/locale mechanics, and even the generator implementation itself
|
|
180
|
+
(`Thecore::Generators::ActionCompanion`) with Root Action:
|
|
181
|
+
|
|
182
|
+
```bash
|
|
183
|
+
rails generate thecore:member_action my_action
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
Same file layout as Root Action (`lib/member_actions/`/`config/member_actions/`,
|
|
187
|
+
`app/views/rails_admin/main/my_action.html.erb`, `.../actions/my_action.js`/`.scss`,
|
|
188
|
+
after_initialize.rb require line, assets.rb precompile line, every-locale-file entry,
|
|
189
|
+
`--atom=NAME`, idempotent re-run). What differs is each action's own template content — not
|
|
190
|
+
unified between the two, matching `addRootAction.js`/`addMemberAction.js`'s own separate
|
|
191
|
+
templates exactly:
|
|
192
|
+
|
|
193
|
+
- **`action.rb`** (server-side, the real behavioral difference) — a RailsAdmin `:member`
|
|
194
|
+
action (`http_methods [:get, :patch]`) whose controller branches on `request.xhr? &&
|
|
195
|
+
request.get?` (returns JSON) vs. `request.patch?` (a form submission, redirects back to the
|
|
196
|
+
record), instead of Root's single `:root` action with a fetch/JSON + `ActionCable.server.broadcast`
|
|
197
|
+
example.
|
|
198
|
+
- **`action.js`/`action.html.erb`** — both still set up the same `ActivityLogChannel`
|
|
199
|
+
ActionCable subscription Root's do; only the test button's click handler differs (a plain
|
|
200
|
+
XHR `GET` here vs. `fetch` there), and the view adds a `form_with(..., method: :patch)` for
|
|
201
|
+
the PATCH half of the example.
|
|
202
|
+
|
|
203
|
+
### `rails thecore:check_practices`
|
|
204
|
+
|
|
205
|
+
A Ruby port of `thecore_code_extension`'s `checkPractices.js` — audits **Scaffold Files**
|
|
206
|
+
(thecore_generators#13), **Models** (thecore_generators#13), and **Actions**
|
|
207
|
+
(thecore_generators#14, `--fix` included).
|
|
208
|
+
|
|
209
|
+
```bash
|
|
210
|
+
rails thecore:check_practices # host app + every ATOM under vendor/submodules/
|
|
211
|
+
rails thecore:check_practices -- --atom=my_atom # scope to a single ATOM
|
|
212
|
+
rails thecore:check_practices -- --json # structured output for CI/the VS Code extension
|
|
213
|
+
rails thecore:check_practices -- --fix # apply every fixable violation, no confirmation
|
|
214
|
+
```
|
|
215
|
+
|
|
216
|
+
The `--` before any flag is the standard Rake convention for passing arguments through to a
|
|
217
|
+
task instead of having Rake's own option parser reject them — see
|
|
218
|
+
[Rake's own docs](https://ruby.github.io/rake/doc/rakefile_rdoc.html#label-Task+Arguments).
|
|
219
|
+
Flags combine freely, e.g. `rails thecore:check_practices -- --atom=my_atom --fix --json`.
|
|
220
|
+
|
|
221
|
+
- **Scaffold Files** — `config/initializers/after_initialize.rb` and `assets.rb` must exist
|
|
222
|
+
and carry their structural marker (`Rails.application.configure do` /
|
|
223
|
+
`Rails.application.config.assets.precompile`). Checked in **both** ATOM and host-app
|
|
224
|
+
context (`checkPractices.js` only ever checked ATOM context). Not fixable.
|
|
225
|
+
- **Models** — rescoped per [ADR 0001](https://github.com/gabrieletassoni/thecore/blob/release/3/docs/adr/0001-application-record-defaults-over-generated-concerns.md):
|
|
226
|
+
a model with **no** `Api::`/`RailsAdmin::` concern is the correct, no-customization default
|
|
227
|
+
and is never flagged. Only two states are violations: a model `include`-ing a concern module
|
|
228
|
+
whose file doesn't exist (`orphan_api_include`/`orphan_rails_admin_include`), or a concern
|
|
229
|
+
file present but missing one of its required markers (`extend ActiveSupport::Concern` plus
|
|
230
|
+
`cattr_accessor :json_attrs` for `Api::`, `rails_admin do` for `RailsAdmin::`). Not fixable —
|
|
231
|
+
regenerating over an existing, hand-edited concern could clobber real customization.
|
|
232
|
+
- **Actions** — scans `root_actions`, `member_actions`, and `collection_actions` (in `lib/` for
|
|
233
|
+
an ATOM, `config/` for the host app), with the same rules for all three:
|
|
234
|
+
`collection_actions` is scanned even though no generator creates files there yet, since a
|
|
235
|
+
hand-written one could already exist and go unreported otherwise. Reports missing/broken
|
|
236
|
+
action-file markers (`RailsAdmin::Config::Actions.add_action`, `http_methods` — never
|
|
237
|
+
fixable), a missing companion view/JS/SCSS or one present but missing its own marker (a
|
|
238
|
+
*missing* companion is fixable for `root_action`/`member_action`, by delegating straight to
|
|
239
|
+
that generator's own template rendering — never for `collection_action`, which has no
|
|
240
|
+
generator to delegate to; an *existing* companion missing a marker is never fixable, same
|
|
241
|
+
reasoning as Models), a missing `after_initialize.rb` require line (fixable, all three
|
|
242
|
+
kinds), and a missing locale entry checked against **every** `*.yml` already present in the
|
|
243
|
+
locales directory, not just `en`/`it` (fixable, all three kinds).
|
|
244
|
+
|
|
245
|
+
Default output is human-readable text grouped by file; `--json` emits
|
|
246
|
+
`{ "violations": [{ "file", "line", "message", "severity", "fixable", "code" }] }` — `code` is
|
|
247
|
+
a stable identifier (e.g. `missing_after_initialize`, `orphan_api_include`,
|
|
248
|
+
`missing_companion_view`) a future consumer can filter on without depending on `message` text.
|
|
249
|
+
`--fix` applies every fixable violation in one pass with no confirmation of its own — whoever
|
|
250
|
+
passes it has already decided — then re-scans and reports/exits based on whatever violations
|
|
251
|
+
remain (so a violation this run can't fix, e.g. a `collection_action` companion, still shows up
|
|
252
|
+
after `--fix`). The task exits non-zero whenever any violation remains, zero otherwise, so it's
|
|
253
|
+
usable as a CI gate either with or without `--fix`.
|
|
254
|
+
|
|
46
255
|
## Installation
|
|
47
256
|
|
|
48
257
|
Add to your host app's or ATOM's `Gemfile`:
|
|
@@ -55,12 +264,24 @@ gem "thecore_generators", "~> 3.0"
|
|
|
55
264
|
|
|
56
265
|
Tests use a `Rails::Generators::TestCase`-based harness against the `test/dummy` Rails
|
|
57
266
|
app included in this repo (needed to exercise generators the way a real host app would).
|
|
267
|
+
`test/dummy` also boots real `model_driven_api`/`thecore_ui_rails_admin` (and their own
|
|
268
|
+
transitive `thecore_backend_commons`/`thecore_auth_commons` dependencies) as temporary
|
|
269
|
+
git-based dependencies — see the Gemfile's comment — purely so
|
|
270
|
+
`test/generators/thecore/model_generator_default_concern_behavior_test.rb` can prove the
|
|
271
|
+
no-concern default actually works at runtime, not just that no file was written.
|
|
58
272
|
|
|
59
273
|
```bash
|
|
60
274
|
bundle install
|
|
61
275
|
bundle exec rake test
|
|
62
276
|
```
|
|
63
277
|
|
|
278
|
+
If your shell has `DATABASE_URL` set to a PostgreSQL URL (e.g. inside the Thecore
|
|
279
|
+
devcontainer), unset it first — it overrides `test/dummy`'s own SQLite3 test config:
|
|
280
|
+
|
|
281
|
+
```bash
|
|
282
|
+
env -u DATABASE_URL bundle exec rake test
|
|
283
|
+
```
|
|
284
|
+
|
|
64
285
|
`bundle exec rake` alone runs the same suite (`test` is the default Rake task).
|
|
65
286
|
|
|
66
287
|
To run a single test file:
|
|
@@ -69,6 +290,16 @@ To run a single test file:
|
|
|
69
290
|
bundle exec ruby -Itest test/generators/thecore/model_generator_test.rb
|
|
70
291
|
```
|
|
71
292
|
|
|
293
|
+
## Who depends on this
|
|
294
|
+
|
|
295
|
+
The host backend app's own `Gemfile` (`:development` group) and the
|
|
296
|
+
[Thecore VS Code extension](https://github.com/gabrieletassoni/thecore_code_extension)'s
|
|
297
|
+
`addModel`/`addMigration` commands both depend on this gem being present to get Thecore-aware
|
|
298
|
+
`rails generate` behavior — the extension actually checks for it and offers to add it
|
|
299
|
+
automatically if it's missing, since without it `rails generate model`/`migration` still "work"
|
|
300
|
+
but silently skip every convention described above. See `CLAUDE.md`'s "Who consumes this gem"
|
|
301
|
+
section for detail.
|
|
302
|
+
|
|
72
303
|
## Releasing
|
|
73
304
|
|
|
74
305
|
Version lives in `lib/thecore_generators/version.rb`. Pushing a commit that bumps it
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
module Thecore
|
|
2
|
+
module Generators
|
|
3
|
+
# Shared helper logic for Thecore's own per-action generators
|
|
4
|
+
# (RootActionGenerator - thecore_generators#11, MemberActionGenerator -
|
|
5
|
+
# thecore_generators#12): everything about them that is NOT the action
|
|
6
|
+
# file's own RailsAdmin action type/template content, and not a Thor
|
|
7
|
+
# *task* method itself.
|
|
8
|
+
#
|
|
9
|
+
# Thor::Group (which Rails::Generators::Base/NamedBase extends)
|
|
10
|
+
# discovers its task list via a `method_added` hook that only fires for
|
|
11
|
+
# methods defined directly, via `def`, in the generator class's own
|
|
12
|
+
# body - never for methods a class merely picks up through `include`.
|
|
13
|
+
# So each of `validate_action_name!`/`create_action_file`/
|
|
14
|
+
# `create_view_js_scss_companions`/`add_after_initialize_require`/
|
|
15
|
+
# `add_assets_precompile_line`/`add_locale_entries` must still be a
|
|
16
|
+
# thin method defined directly on RootActionGenerator/
|
|
17
|
+
# MemberActionGenerator themselves (identical one-liners in both); only
|
|
18
|
+
# the private logic those methods delegate to lives here.
|
|
19
|
+
#
|
|
20
|
+
# Extracted once both generators existed and turned out identical apart
|
|
21
|
+
# from that thin task-method layer and their directory name
|
|
22
|
+
# ("root_actions"/"member_actions") - not a speculative abstraction
|
|
23
|
+
# built ahead of a second user.
|
|
24
|
+
#
|
|
25
|
+
# The including class must declare `action_kind "root_action"` (or
|
|
26
|
+
# `"member_action"`) at the class body level, which derives both the
|
|
27
|
+
# directory name (pluralized: "root_actions"/"member_actions") and the
|
|
28
|
+
# wording used in the name-validation error message.
|
|
29
|
+
module ActionCompanion
|
|
30
|
+
def self.included(base)
|
|
31
|
+
base.extend(ClassMethods)
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
# Shared require-line text for an action's `after_initialize.rb`
|
|
35
|
+
# registration - `lib/<kind>s/<name>` in ATOM context (on the load
|
|
36
|
+
# path via the gemspec), a full `Rails.root.join('config', ...)` path
|
|
37
|
+
# in host-app context (`config/` isn't on the load path). A plain
|
|
38
|
+
# module method (not routed through `ClassMethods`/`extend`, unlike
|
|
39
|
+
# `action_kind` below) so it's callable directly as
|
|
40
|
+
# `Thecore::Generators::ActionCompanion.require_line_for(...)` without
|
|
41
|
+
# an including generator instance - used both by `require_line` below
|
|
42
|
+
# (via a real generator's own `atom_dir`/`file_name`) and directly by
|
|
43
|
+
# `Thecore::CheckPractices`' Actions check, so the audit's own
|
|
44
|
+
# expectation can never drift out of sync with what a real Root/Member
|
|
45
|
+
# Action generator actually writes.
|
|
46
|
+
def self.require_line_for(kind:, in_atom:, name:)
|
|
47
|
+
dir_name = "#{kind}s"
|
|
48
|
+
if in_atom
|
|
49
|
+
"require '#{dir_name}/#{name}'"
|
|
50
|
+
else
|
|
51
|
+
"require Rails.root.join('config', '#{dir_name}', '#{name}').to_s"
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
# A class-level accessor, not an instance `define_method`, and
|
|
56
|
+
# deliberately so: any *instance* method this DSL defined directly on
|
|
57
|
+
# the including generator class - even a private one, since Thor's
|
|
58
|
+
# `method_added` hook fires and registers it as a task at the moment
|
|
59
|
+
# `define_method` returns, before a later `private` call could demote
|
|
60
|
+
# it - would itself become a spurious Thor task (this was tried and
|
|
61
|
+
# caught via `RootActionGenerator.all_tasks.keys` including
|
|
62
|
+
# "action_kind" as an actual, if harmless, generator step). Storing
|
|
63
|
+
# the value in a class-level ivar instead, read back via
|
|
64
|
+
# `self.class.action_kind` from the private instance methods below,
|
|
65
|
+
# adds no method to the generator class's own instance side at all.
|
|
66
|
+
module ClassMethods
|
|
67
|
+
def action_kind(value = nil)
|
|
68
|
+
@action_kind = value unless value.nil?
|
|
69
|
+
@action_kind
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
private
|
|
74
|
+
|
|
75
|
+
def validate_action_name_for_kind!
|
|
76
|
+
return if name.match?(/\A[a-z_][a-z0-9_]*\z/)
|
|
77
|
+
|
|
78
|
+
raise Thor::Error,
|
|
79
|
+
"'#{name}' is not a valid #{self.class.action_kind.tr("_", " ")} name - use " \
|
|
80
|
+
"snake_case, starting with a lowercase letter or underscore (a leading digit would " \
|
|
81
|
+
"make the generated `topic: :#{name}` symbol invalid Ruby)."
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
def action_dir_name
|
|
85
|
+
"#{self.class.action_kind}s"
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
# lib/<action_dir_name> in ATOM context (on the load path via the
|
|
89
|
+
# gemspec), config/<action_dir_name> in host-app context (not on the
|
|
90
|
+
# load path, hence the full-path require in `require_line` below) -
|
|
91
|
+
# see docs/adr/0001-main-app-actions-live-in-config.md in
|
|
92
|
+
# thecore_code_extension.
|
|
93
|
+
def action_file_path
|
|
94
|
+
base_dir = atom_dir ? "lib" : "config"
|
|
95
|
+
File.join(base_dir, action_dir_name, "#{file_name}.rb")
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
def require_line
|
|
99
|
+
ActionCompanion.require_line_for(kind: self.class.action_kind, in_atom: !atom_dir.nil?, name: file_name)
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
def assets_precompile_line
|
|
103
|
+
"Rails.application.config.assets.precompile += %w( rails_admin/actions/#{file_name}.js " \
|
|
104
|
+
"rails_admin/actions/#{file_name}.css )"
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
def action_name_camel_case
|
|
108
|
+
file_name.downcase.gsub(/[-_]([a-z0-9])/) { Regexp.last_match(1).upcase }
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
def title_case_name
|
|
112
|
+
file_name.split("_").map(&:capitalize).join(" ")
|
|
113
|
+
end
|
|
114
|
+
end
|
|
115
|
+
end
|
|
116
|
+
end
|
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
require "generators/thecore/workspace_context"
|
|
2
|
+
|
|
3
|
+
module Thecore
|
|
4
|
+
module Generators
|
|
5
|
+
# Implements ADR 0003 in the thecore repo
|
|
6
|
+
# (docs/adr/0003-migration-driven-inverse-association-wiring.md):
|
|
7
|
+
# Rails' own migration generator only ever wires the owning (`belongs_to`)
|
|
8
|
+
# side of a `references`/`add_reference` column — the inverse
|
|
9
|
+
# (`has_many`/`has_one`) has always been manual. This module detects
|
|
10
|
+
# `references` attributes on the migration being generated and writes the
|
|
11
|
+
# missing inverse side into the *target* model's canonical per-model
|
|
12
|
+
# concern, following the cross-ATOM extension pattern GUIDE.md §4.5
|
|
13
|
+
# already documents (`config/initializers/concern_<model>.rb` plus a
|
|
14
|
+
# `TargetModel.send(:include, ...)` registered in
|
|
15
|
+
# `config/initializers/after_initialize.rb`).
|
|
16
|
+
#
|
|
17
|
+
# Included by both Thecore::Generators::MigrationGenerator (standalone
|
|
18
|
+
# `rails generate migration ... x:references`) and
|
|
19
|
+
# Thecore::Generators::ModelGenerator (`rails generate model Foo
|
|
20
|
+
# x:references` delegates its own migration creation to
|
|
21
|
+
# ActiveRecord::Generators::ModelGenerator#create_migration_file, a
|
|
22
|
+
# *different* method than MigrationGenerator's own, so both host classes
|
|
23
|
+
# need their own hook). Both wire it the same way: override
|
|
24
|
+
# `create_migration_file`, call `super`, then
|
|
25
|
+
# `wire_inverse_associations_from_references` — matching the
|
|
26
|
+
# override-and-call-super style Thecore::Generators::ModelGenerator
|
|
27
|
+
# already uses for `create_model_file`/`add_default_concerns`.
|
|
28
|
+
#
|
|
29
|
+
# Requires the includer to already provide (directly or via
|
|
30
|
+
# Thecore::Generators::AtomAware, included by both host classes):
|
|
31
|
+
# - `attributes` — Rails::Generators::GeneratedAttribute array (from
|
|
32
|
+
# ActiveRecord's own migration/model generator `argument`)
|
|
33
|
+
# - `table_name` — Rails::Generators::NamedBase; memoized into
|
|
34
|
+
# @table_name, which for MigrationGenerator is populated by
|
|
35
|
+
# ActiveRecord's own `set_local_assigns!` (run during `super`) from
|
|
36
|
+
# the migration's file name (`add_x_to_y`/`create_y`), and for
|
|
37
|
+
# ModelGenerator resolves directly from the model's own class name -
|
|
38
|
+
# either way it names the table/model *gaining* the reference.
|
|
39
|
+
# - `destination_root`/`host_app_root` — Thecore::Generators::AtomAware
|
|
40
|
+
module AssociationWiring
|
|
41
|
+
HEADER_COMMENT = <<~RUBY.freeze
|
|
42
|
+
# This file is maintained by thecore_generators (migration generator's
|
|
43
|
+
# inverse-association wiring - see
|
|
44
|
+
# docs/adr/0003-migration-driven-inverse-association-wiring.md in the
|
|
45
|
+
# thecore repo). Associations below are appended automatically whenever
|
|
46
|
+
# a `references` column targeting this model is added elsewhere.
|
|
47
|
+
# Do not hand-edit the generated section: a future generator run may
|
|
48
|
+
# append to it again, and hand edits are not accounted for.
|
|
49
|
+
RUBY
|
|
50
|
+
|
|
51
|
+
AFTER_INITIALIZE_TEMPLATE = <<~RUBY.freeze
|
|
52
|
+
Rails.application.configure do
|
|
53
|
+
config.after_initialize do
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
RUBY
|
|
57
|
+
|
|
58
|
+
def self.included(base)
|
|
59
|
+
base.class_option :non_interactive, type: :boolean, default: false,
|
|
60
|
+
desc: "Skip the inverse-association cardinality prompt and default to has_many " \
|
|
61
|
+
"(no real TTY is available to CI or scripted invocations, e.g. the VS Code " \
|
|
62
|
+
"extension)"
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
private
|
|
66
|
+
|
|
67
|
+
# Entry point, called after `super` from the includer's own
|
|
68
|
+
# `create_migration_file` override.
|
|
69
|
+
def wire_inverse_associations_from_references
|
|
70
|
+
return if options[:migration] == false # ModelGenerator's --no-migration
|
|
71
|
+
return if attributes.nil?
|
|
72
|
+
|
|
73
|
+
owning_table_name = table_name
|
|
74
|
+
return if owning_table_name.blank?
|
|
75
|
+
|
|
76
|
+
reference_attributes = attributes.select(&:reference?)
|
|
77
|
+
return if reference_attributes.empty?
|
|
78
|
+
|
|
79
|
+
reference_attributes.each { |attribute| wire_inverse_association(owning_table_name, attribute) }
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
def wire_inverse_association(owning_table_name, attribute)
|
|
83
|
+
target_class_name = attribute.singular_name.camelize
|
|
84
|
+
cardinality = prompt_cardinality_for(target_class_name)
|
|
85
|
+
return if cardinality == :skip
|
|
86
|
+
|
|
87
|
+
association_line = association_line_for(cardinality, owning_table_name)
|
|
88
|
+
write_target_concern(target_class_name, association_line)
|
|
89
|
+
register_after_initialize(target_class_name)
|
|
90
|
+
log_cross_boundary_dependency(target_class_name)
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
def association_line_for(cardinality, owning_table_name)
|
|
94
|
+
if cardinality == :has_one
|
|
95
|
+
"has_one :#{owning_table_name.singularize}"
|
|
96
|
+
else
|
|
97
|
+
"has_many :#{owning_table_name}"
|
|
98
|
+
end
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
def prompt_cardinality_for(target_class_name)
|
|
102
|
+
return :has_many unless interactive_association_prompt?
|
|
103
|
+
|
|
104
|
+
answer = ask(
|
|
105
|
+
"Inverse association on #{target_class_name} for this reference " \
|
|
106
|
+
"(has_many/has_one/skip)?",
|
|
107
|
+
default: "has_many",
|
|
108
|
+
limited_to: %w[has_many has_one skip]
|
|
109
|
+
)
|
|
110
|
+
answer.to_s.strip.to_sym
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
# No real TTY is present for CI/scripted/extension invocations (or the
|
|
114
|
+
# includer was explicitly told not to prompt via --non-interactive) -
|
|
115
|
+
# default straight to has_many, per ADR 0003.
|
|
116
|
+
def interactive_association_prompt?
|
|
117
|
+
!options[:non_interactive] && $stdin.tty? && $stdout.tty?
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
def concern_path_for(target_class_name)
|
|
121
|
+
"config/initializers/concern_#{target_class_name.underscore}.rb"
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
def concern_module_name_for(target_class_name)
|
|
125
|
+
"Concern#{target_class_name}"
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
def write_target_concern(target_class_name, association_line)
|
|
129
|
+
concern_path = concern_path_for(target_class_name)
|
|
130
|
+
|
|
131
|
+
if File.exist?(File.join(destination_root, concern_path))
|
|
132
|
+
insert_association_into_existing_concern(concern_path, association_line)
|
|
133
|
+
else
|
|
134
|
+
create_concern_file(concern_path, concern_module_name_for(target_class_name), association_line)
|
|
135
|
+
end
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
def create_concern_file(concern_path, concern_module_name, association_line)
|
|
139
|
+
content = <<~RUBY
|
|
140
|
+
#{HEADER_COMMENT}
|
|
141
|
+
module #{concern_module_name}
|
|
142
|
+
extend ActiveSupport::Concern
|
|
143
|
+
|
|
144
|
+
included do
|
|
145
|
+
#{association_line}
|
|
146
|
+
end
|
|
147
|
+
end
|
|
148
|
+
RUBY
|
|
149
|
+
|
|
150
|
+
create_file(concern_path, content)
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
def insert_association_into_existing_concern(concern_path, association_line)
|
|
154
|
+
full_path = File.join(destination_root, concern_path)
|
|
155
|
+
content = File.read(full_path)
|
|
156
|
+
|
|
157
|
+
if content.match?(/^\s*#{Regexp.escape(association_line)}\s*$/)
|
|
158
|
+
say_status :skip, "#{concern_path} already has `#{association_line}`", :blue
|
|
159
|
+
return
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
insert_into_file(concern_path, " #{association_line}\n", after: /included do\n/)
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
def register_after_initialize(target_class_name)
|
|
166
|
+
path = "config/initializers/after_initialize.rb"
|
|
167
|
+
full_path = File.join(destination_root, path)
|
|
168
|
+
|
|
169
|
+
create_file(path, AFTER_INITIALIZE_TEMPLATE) unless File.exist?(full_path)
|
|
170
|
+
|
|
171
|
+
registration_line = "#{target_class_name}.send(:include, #{concern_module_name_for(target_class_name)})"
|
|
172
|
+
content = File.read(full_path)
|
|
173
|
+
return if content.include?(registration_line)
|
|
174
|
+
|
|
175
|
+
insert_into_file(path, " #{registration_line}\n", after: /config\.after_initialize do\n/)
|
|
176
|
+
end
|
|
177
|
+
|
|
178
|
+
# Cross-boundary case (ADR 0003): the concern always lives in the
|
|
179
|
+
# invoking app/ATOM (destination_root), never the target model's own -
|
|
180
|
+
# so when the target model actually lives elsewhere, the generated
|
|
181
|
+
# `TargetModel.send(:include, ...)` line depends on that model's
|
|
182
|
+
# constant being loaded, which requires a real gem/path dependency a
|
|
183
|
+
# human has to add. We only log it — never edit a gemspec/Gemfile.
|
|
184
|
+
def log_cross_boundary_dependency(target_class_name)
|
|
185
|
+
target_root = Thecore::Generators::WorkspaceContext.model_root_for(
|
|
186
|
+
class_name: target_class_name, app_root: host_app_root
|
|
187
|
+
)
|
|
188
|
+
return if target_root.nil? || target_root == destination_root
|
|
189
|
+
|
|
190
|
+
own_is_atom = destination_root != host_app_root
|
|
191
|
+
target_is_atom = target_root != host_app_root
|
|
192
|
+
own_label = own_is_atom ? File.basename(destination_root) : "the host app"
|
|
193
|
+
target_label = target_is_atom ? File.basename(target_root) : "the host app"
|
|
194
|
+
|
|
195
|
+
message = "Cross-boundary reference: #{target_class_name} lives in #{target_label}, but the " \
|
|
196
|
+
"generated concern was written into #{own_label} (ADR 0003: the concern always lives in the " \
|
|
197
|
+
"invoking app/ATOM). #{dependency_hint(own_is_atom, target_is_atom, target_root)}"
|
|
198
|
+
|
|
199
|
+
say_status :dependency, message, :yellow
|
|
200
|
+
end
|
|
201
|
+
|
|
202
|
+
def dependency_hint(own_is_atom, target_is_atom, target_root)
|
|
203
|
+
target_gem_name = File.basename(target_root)
|
|
204
|
+
|
|
205
|
+
if own_is_atom && target_is_atom
|
|
206
|
+
"Add a gem dependency in this ATOM's gemspec, e.g.: spec.add_dependency \"#{target_gem_name}\""
|
|
207
|
+
elsif !own_is_atom && target_is_atom
|
|
208
|
+
"Add a Gemfile dependency in the host app, e.g.: gem \"#{target_gem_name}\", " \
|
|
209
|
+
"path: \"vendor/submodules/#{target_gem_name}\""
|
|
210
|
+
else
|
|
211
|
+
"The target model lives in the host app; confirm this ATOM is meant to reference host-app " \
|
|
212
|
+
"code directly (no gemspec/Gemfile dependency line applies here)."
|
|
213
|
+
end
|
|
214
|
+
end
|
|
215
|
+
end
|
|
216
|
+
end
|
|
217
|
+
end
|