thecore_generators 3.6.0 → 3.11.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 +166 -14
- data/lib/generators/thecore/association_wiring.rb +2 -1
- data/lib/generators/thecore/atom/atom_generator.rb +481 -0
- data/lib/generators/thecore/atom/templates/abilities.rb.tt +16 -0
- data/lib/generators/thecore/atom/templates/seeds.rb.tt +1 -0
- data/lib/generators/thecore/collection_action/collection_action_generator.rb +64 -0
- data/lib/generators/thecore/collection_action/templates/action.html.erb.tt +13 -0
- data/lib/generators/thecore/collection_action/templates/action.js.tt +42 -0
- data/lib/generators/thecore/collection_action/templates/action.rb.tt +33 -0
- data/lib/generators/thecore/collection_action/templates/action.scss.tt +38 -0
- data/lib/generators/thecore/sample_fetcher.rb +68 -0
- data/lib/generators/thecore/tty_detection.rb +19 -0
- data/lib/templates/app_template.rb +220 -0
- data/lib/thecore_generators/check_practices.rb +20 -14
- data/lib/thecore_generators/version.rb +1 -1
- metadata +12 -1
|
@@ -0,0 +1,481 @@
|
|
|
1
|
+
require "rails/generators/named_base"
|
|
2
|
+
require "generators/thecore/tty_detection"
|
|
3
|
+
require "generators/thecore/sample_fetcher"
|
|
4
|
+
require "shellwords"
|
|
5
|
+
require "yaml"
|
|
6
|
+
|
|
7
|
+
module Thecore
|
|
8
|
+
module Generators
|
|
9
|
+
# `rails generate thecore:atom NAME` (thecore_generators#20, per ADR 0006 in the
|
|
10
|
+
# thecore repo) — a Ruby port of thecore_code_extension's createATOM.js, producing
|
|
11
|
+
# a complete, working ATOM end-to-end from a terminal: no VS Code, no extension
|
|
12
|
+
# required.
|
|
13
|
+
#
|
|
14
|
+
# Unlike every other generator in this gem, AtomGenerator does NOT include
|
|
15
|
+
# Thecore::Generators::AtomAware and takes no --atom=NAME option — creating a
|
|
16
|
+
# *new* ATOM is only ever a host-app-root operation (it makes no sense to run
|
|
17
|
+
# "from inside" an ATOM that doesn't exist yet). destination_root therefore stays
|
|
18
|
+
# fixed at the host app root for the whole generator; every path this class writes
|
|
19
|
+
# is expressed relative to that root via #atom_root ("vendor/submodules/<name>"),
|
|
20
|
+
# not via AtomAware's destination_root-redirection trick.
|
|
21
|
+
#
|
|
22
|
+
# Two Gemfiles are in play here — the host app's own (destination_root/Gemfile)
|
|
23
|
+
# and the freshly-scaffolded ATOM's own (vendor/submodules/<name>/Gemfile) — which
|
|
24
|
+
# is why Rails::Generators::Actions#gem (used once, in #add_gem_to_host_gemfile)
|
|
25
|
+
# can't be reused for the ATOM's own Gemfile: #gem's own implementation always
|
|
26
|
+
# writes through `in_root { ... }`, and Thor::Actions#in_root is hardcoded to
|
|
27
|
+
# `@destination_stack.first` (the *original* destination_root), not whatever
|
|
28
|
+
# #inside might currently have pushed — so it can never be redirected to a nested
|
|
29
|
+
# path. The ATOM's own Gemfile is instead mutated via plain #append_to_file calls
|
|
30
|
+
# against an explicit relative path.
|
|
31
|
+
class AtomGenerator < Rails::Generators::NamedBase
|
|
32
|
+
class_option :non_interactive, type: :boolean, default: false,
|
|
33
|
+
desc: "Skip all interactive prompts; every required value must be supplied " \
|
|
34
|
+
"via --summary/--description/--author/--email/--url, and the " \
|
|
35
|
+
"API/Admin dependency choice defaults to included unless " \
|
|
36
|
+
"--skip-api-admin-deps is also passed"
|
|
37
|
+
class_option :summary, type: :string, default: nil,
|
|
38
|
+
desc: "The ATOM's one-line summary (required with --non-interactive)"
|
|
39
|
+
class_option :description, type: :string, default: nil,
|
|
40
|
+
desc: "The ATOM's longer description (required with --non-interactive)"
|
|
41
|
+
class_option :author, type: :string, default: nil,
|
|
42
|
+
desc: "The ATOM's author name (required with --non-interactive)"
|
|
43
|
+
class_option :email, type: :string, default: nil,
|
|
44
|
+
desc: "The ATOM author's email (required with --non-interactive)"
|
|
45
|
+
class_option :url, type: :string, default: nil,
|
|
46
|
+
desc: "The ATOM's homepage URL (required with --non-interactive)"
|
|
47
|
+
class_option :skip_api_admin_deps, type: :boolean, default: false,
|
|
48
|
+
desc: "Don't add model_driven_api/thecore_ui_rails_admin as dependencies " \
|
|
49
|
+
"(only consulted with --non-interactive; interactively this is its " \
|
|
50
|
+
"own yes/no prompt, default yes)"
|
|
51
|
+
|
|
52
|
+
source_root File.expand_path("templates", __dir__)
|
|
53
|
+
|
|
54
|
+
# Validated the same way as createATOM.js's own six prompts: every field must
|
|
55
|
+
# be present; email must contain "@"; url must start with "http". Order matches
|
|
56
|
+
# the original prompt sequence.
|
|
57
|
+
REQUIRED_STRING_FIELDS = {
|
|
58
|
+
"summary" => ->(v) { !v.to_s.strip.empty? },
|
|
59
|
+
"description" => ->(v) { !v.to_s.strip.empty? },
|
|
60
|
+
"author" => ->(v) { !v.to_s.strip.empty? },
|
|
61
|
+
"email" => ->(v) { v.to_s.include?("@") },
|
|
62
|
+
"url" => ->(v) { v.to_s.start_with?("http") },
|
|
63
|
+
}.freeze
|
|
64
|
+
|
|
65
|
+
# This gem's own ADR 0001 floor (matching the App template's already-corrected
|
|
66
|
+
# versions) — not createATOM.js's stale "~> 3.1"/"~> 3.2".
|
|
67
|
+
MODEL_DRIVEN_API_VERSION = "~> 3.9"
|
|
68
|
+
THECORE_UI_RAILS_ADMIN_VERSION = "~> 3.8"
|
|
69
|
+
|
|
70
|
+
# Faithful port of templates/createATOM/after_initialize.rb /
|
|
71
|
+
# templates/createATOM/assets.rb in thecore_code_extension — static content,
|
|
72
|
+
# no per-ATOM interpolation, so these are plain constants rather than .tt files.
|
|
73
|
+
AFTER_INITIALIZE_CONTENT = <<~RUBY
|
|
74
|
+
Rails.application.configure do
|
|
75
|
+
config.after_initialize do
|
|
76
|
+
# For example, it can be used to load a root action defined in lib, for example:
|
|
77
|
+
# require 'root_actions/tcp_debug'
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
RUBY
|
|
81
|
+
|
|
82
|
+
ASSETS_CONTENT = <<~RUBY
|
|
83
|
+
# PLEASE, uncomment if needed.
|
|
84
|
+
# For Example: in the case there's a root action called tcp_debug, add the following lines to include css and javascripts for auto loading:
|
|
85
|
+
# Rails.application.config.assets.precompile += %w(
|
|
86
|
+
# main_tcp_debug.js
|
|
87
|
+
# main_tcp_debug.css
|
|
88
|
+
# )
|
|
89
|
+
RUBY
|
|
90
|
+
|
|
91
|
+
# Faithful port of createATOM.js's addCICDFiles gempush.yml, with its two
|
|
92
|
+
# long-standing bugs fixed (thecore_generators#20 acceptance criteria): the awk
|
|
93
|
+
# pipeline computing the version string had a stray, unmatched `)` instead of a
|
|
94
|
+
# closing `}'`; and `version_exists` was referenced in two steps' `if:`
|
|
95
|
+
# conditions but never actually set anywhere (the "check" step only ever did
|
|
96
|
+
# `echo $?`), so those two steps have never run for any ATOM generated this way.
|
|
97
|
+
# Fixed here by actually writing `version_exists` to $GITHUB_ENV based on
|
|
98
|
+
# whether a git tag for the computed version already exists - kept as an
|
|
99
|
+
# `env.*`-style if: condition (not switched to `steps.*.outputs.*`) to stay as
|
|
100
|
+
# close to the original's structure as the fix allows.
|
|
101
|
+
GEMPUSH_YML_CONTENT = <<~YAML
|
|
102
|
+
name: Ruby Gem
|
|
103
|
+
on: push
|
|
104
|
+
jobs:
|
|
105
|
+
build:
|
|
106
|
+
name: Build + Publish
|
|
107
|
+
runs-on: ubuntu-latest
|
|
108
|
+
steps:
|
|
109
|
+
- uses: actions/checkout@v3
|
|
110
|
+
- name: Check if version already exists
|
|
111
|
+
run: |
|
|
112
|
+
version=$(grep -oP 'VERSION = "\\K[^"]+' lib/*/version.rb | awk -F'.' '{print $1"."$2"."$3}')
|
|
113
|
+
git fetch --unshallow --tags
|
|
114
|
+
if git rev-parse "$version" >/dev/null 2>&1; then
|
|
115
|
+
echo "version_exists=true" >> "$GITHUB_ENV"
|
|
116
|
+
else
|
|
117
|
+
echo "version_exists=false" >> "$GITHUB_ENV"
|
|
118
|
+
fi
|
|
119
|
+
- name: Set git tag
|
|
120
|
+
if: env.version_exists == 'false'
|
|
121
|
+
run: |
|
|
122
|
+
git config --local user.email "noreply@alchemic.it"
|
|
123
|
+
git config --local user.name "AlchemicIT"
|
|
124
|
+
version=$(grep -oP 'VERSION = "\\K[^"]+' lib/*/version.rb | awk -F'.' '{print $1"."$2"."$3}')
|
|
125
|
+
git tag -a $version -m "Version $version"
|
|
126
|
+
git push --tags
|
|
127
|
+
- name: Publish to RubyGems
|
|
128
|
+
if: env.version_exists == 'false'
|
|
129
|
+
env:
|
|
130
|
+
GEM_HOST_API_KEY: ${{secrets.RUBYGEMS_AUTH_TOKEN}}
|
|
131
|
+
run: |
|
|
132
|
+
mkdir -p $HOME/.gem
|
|
133
|
+
touch $HOME/.gem/credentials
|
|
134
|
+
chmod 0600 $HOME/.gem/credentials
|
|
135
|
+
printf -- "---\\n:rubygems_api_key: ${GEM_HOST_API_KEY}\\n" > $HOME/.gem/credentials
|
|
136
|
+
gem build *.gemspec
|
|
137
|
+
gem push *.gem
|
|
138
|
+
YAML
|
|
139
|
+
|
|
140
|
+
SCAFFOLD_DIRECTORIES = %w[
|
|
141
|
+
db/migrate
|
|
142
|
+
app/models/concerns/api
|
|
143
|
+
app/models/concerns/rails_admin
|
|
144
|
+
config/initializers
|
|
145
|
+
config/locales
|
|
146
|
+
lib/root_actions
|
|
147
|
+
lib/member_actions
|
|
148
|
+
lib/collection_actions
|
|
149
|
+
app/assets/javascripts
|
|
150
|
+
app/assets/stylesheets
|
|
151
|
+
app/views/rails_admin/main
|
|
152
|
+
.github/workflows
|
|
153
|
+
].freeze
|
|
154
|
+
|
|
155
|
+
# Gem-name convention (matches real examples in this very ecosystem, including
|
|
156
|
+
# the hyphenated `thecore-spot-overrides`) - lowercase, starting with a letter,
|
|
157
|
+
# letters/digits/underscore/hyphen only. Deliberately stricter than NamedBase's
|
|
158
|
+
# own permissive `name` parsing: without this, a name containing a space or
|
|
159
|
+
# shell metacharacter would flow straight into the unescaped shell-out in
|
|
160
|
+
# #create_rails_engine (word-splitting or, worse, executing arbitrary shell),
|
|
161
|
+
# and a namespaced name (`acme/widget`) would desync #atom_root (which uses
|
|
162
|
+
# only #file_name, "widget") from #class_name (which uses the full namespaced
|
|
163
|
+
# "Acme::Widget"), breaking the abilities.rb template.
|
|
164
|
+
NAME_PATTERN = /\A[a-z][a-z0-9_-]*\z/
|
|
165
|
+
|
|
166
|
+
def validate_atom_name!
|
|
167
|
+
return if file_name.match?(NAME_PATTERN)
|
|
168
|
+
|
|
169
|
+
raise Thor::Error,
|
|
170
|
+
"'#{file_name}' is not a valid ATOM name - use lowercase letters, digits, " \
|
|
171
|
+
"underscores, or hyphens, starting with a letter (e.g. tcp_debugger)."
|
|
172
|
+
end
|
|
173
|
+
|
|
174
|
+
def ensure_submodules_dir_exists!
|
|
175
|
+
return if File.directory?(File.join(destination_root, "vendor", "submodules"))
|
|
176
|
+
|
|
177
|
+
raise Thor::Error,
|
|
178
|
+
"vendor/submodules does not exist under #{destination_root} - run `rails generate " \
|
|
179
|
+
"thecore:atom` from a Thecore host app root that already has it (see the App " \
|
|
180
|
+
"application template, thecore_generators#17/#18) before creating an ATOM."
|
|
181
|
+
end
|
|
182
|
+
|
|
183
|
+
# `rails plugin new`'s own `-f` (force) suppresses its normal file-collision
|
|
184
|
+
# prompt, so without this guard a name colliding with an existing ATOM (a real
|
|
185
|
+
# scenario in this very host app: `vendor/submodules/mytask` already exists)
|
|
186
|
+
# would silently overwrite that ATOM's working tree with freshly-generated
|
|
187
|
+
# plugin skeleton files - caught during review, reproduced directly against
|
|
188
|
+
# this app's own real `mytask` submodule.
|
|
189
|
+
def ensure_atom_does_not_already_exist!
|
|
190
|
+
return unless File.exist?(File.join(destination_root, atom_root))
|
|
191
|
+
|
|
192
|
+
raise Thor::Error,
|
|
193
|
+
"#{atom_root} already exists - choose a different name, or remove it first if you " \
|
|
194
|
+
"really mean to regenerate it."
|
|
195
|
+
end
|
|
196
|
+
|
|
197
|
+
def validate_non_interactive_options!
|
|
198
|
+
return unless effectively_non_interactive?
|
|
199
|
+
|
|
200
|
+
missing = REQUIRED_STRING_FIELDS.reject { |key, valid| valid.call(options[key]) }.keys
|
|
201
|
+
return if missing.empty?
|
|
202
|
+
|
|
203
|
+
raise Thor::Error,
|
|
204
|
+
"Missing required flags for --non-interactive: #{missing.map { |k| "--#{k}" }.join(", ")}"
|
|
205
|
+
end
|
|
206
|
+
|
|
207
|
+
def collect_metadata
|
|
208
|
+
@summary = required_field("summary", "the summary of the ATOM, i.e. TCP Debugger")
|
|
209
|
+
@description = required_field("description", "the description of the ATOM, i.e. TCP Debugger")
|
|
210
|
+
@author = required_field("author", "the author of the ATOM, i.e. Alchemic IT")
|
|
211
|
+
@email = required_field("email", "the email of the ATOM author")
|
|
212
|
+
@url = required_field("url", "the url of the ATOM")
|
|
213
|
+
end
|
|
214
|
+
|
|
215
|
+
def collect_api_admin_deps_choice
|
|
216
|
+
@include_api_admin_deps =
|
|
217
|
+
if effectively_non_interactive?
|
|
218
|
+
!options[:skip_api_admin_deps]
|
|
219
|
+
else
|
|
220
|
+
ask(
|
|
221
|
+
"Include model_driven_api/thecore_ui_rails_admin as dependencies?",
|
|
222
|
+
default: "yes", limited_to: %w[yes no]
|
|
223
|
+
) == "yes"
|
|
224
|
+
end
|
|
225
|
+
end
|
|
226
|
+
|
|
227
|
+
# Faithful to createATOM.js's own `rails plugin new "<path>" -fG
|
|
228
|
+
# --skip-gemfile-entry --skip-hotwire --full` invocation, cwd'd to
|
|
229
|
+
# vendor/submodules (via #inside, which - unlike file-writing actions below -
|
|
230
|
+
# genuinely changes the OS process's cwd for #run's sake). `bundle exec` is a
|
|
231
|
+
# deliberate addition over the JS original (which shells a bare `rails`,
|
|
232
|
+
# relying entirely on whatever's globally on PATH): it makes gem resolution
|
|
233
|
+
# explicit rather than incidental, and costs nothing in the real host-app case,
|
|
234
|
+
# where cwd already sits under that app's own Gemfile either way. `file_name`
|
|
235
|
+
# is Shellwords-escaped even though #validate_atom_name! already restricts it
|
|
236
|
+
# to a shell-safe character set - defense in depth, matching the same care
|
|
237
|
+
# #git_init_and_commit already takes with the free-text @author/@email.
|
|
238
|
+
def create_rails_engine
|
|
239
|
+
inside("vendor/submodules") do
|
|
240
|
+
run("bundle exec rails plugin new #{Shellwords.escape(file_name)} " \
|
|
241
|
+
"-fG --skip-gemfile-entry --skip-hotwire --full",
|
|
242
|
+
abort_on_failure: true)
|
|
243
|
+
end
|
|
244
|
+
end
|
|
245
|
+
|
|
246
|
+
# #create_file already `mkdir_p`s its own parent directory, so a separate
|
|
247
|
+
# #empty_directory call per entry would just be redundant work (and, for the
|
|
248
|
+
# 3 of these 12 that #create_scaffold_files/#create_locale_files/
|
|
249
|
+
# #create_ci_files populate with a real file moments later, entirely so).
|
|
250
|
+
def create_scaffold_directories
|
|
251
|
+
SCAFFOLD_DIRECTORIES.each { |dir| create_file(File.join(atom_root, dir, ".keep")) }
|
|
252
|
+
end
|
|
253
|
+
|
|
254
|
+
def create_scaffold_files
|
|
255
|
+
create_file File.join(atom_root, "config/initializers/after_initialize.rb"), AFTER_INITIALIZE_CONTENT
|
|
256
|
+
create_file File.join(atom_root, "config/initializers/add_to_db_migration.rb"),
|
|
257
|
+
"Rails.application.config.paths['db/migrate'] << File.expand_path(\"../../db/migrate\", __dir__)\n"
|
|
258
|
+
create_file File.join(atom_root, "config/initializers/assets.rb"), ASSETS_CONTENT
|
|
259
|
+
template "abilities.rb.tt", File.join(atom_root, "config/initializers/abilities.rb")
|
|
260
|
+
template "seeds.rb.tt", File.join(atom_root, "db/seeds.rb")
|
|
261
|
+
end
|
|
262
|
+
|
|
263
|
+
# Bare "en:\n"/"it:\n", matching the exact convention
|
|
264
|
+
# Thecore::Generators::CompanionFiles#write_action_locale_entries! already uses
|
|
265
|
+
# for this identical "no locale file yet" bootstrap case elsewhere in this gem
|
|
266
|
+
# - not the YAML-document-with-null-value shape `{"en"=>nil}.to_yaml` produces,
|
|
267
|
+
# which is equivalent once parsed but an unnecessary second on-disk convention
|
|
268
|
+
# for the same thing.
|
|
269
|
+
def create_locale_files
|
|
270
|
+
create_file File.join(atom_root, "config/locales/en.yml"), "en:\n"
|
|
271
|
+
create_file File.join(atom_root, "config/locales/it.yml"), "it:\n"
|
|
272
|
+
end
|
|
273
|
+
|
|
274
|
+
def create_ci_files
|
|
275
|
+
create_file File.join(atom_root, ".github/workflows/gempush.yml"), GEMPUSH_YML_CONTENT
|
|
276
|
+
|
|
277
|
+
gitlab_ci = {
|
|
278
|
+
"image" => "gabrieletassoni/vscode-devcontainers-thecore:3",
|
|
279
|
+
"variables" => {
|
|
280
|
+
"GITLAB_EMAIL" => @email,
|
|
281
|
+
"GITLAB_USER_NAME" => @author,
|
|
282
|
+
"GITLAB_GEM_REPO_TARGET" => 'https://${GEM_HOST}/',
|
|
283
|
+
"GEM_HOST_API_KEY" => '${GEMS_REPO_CREDENTIALS}',
|
|
284
|
+
},
|
|
285
|
+
"stages" => %w[build release],
|
|
286
|
+
"build_gem" => {
|
|
287
|
+
"rules" => [{ "if" => "$CI_COMMIT_TAG", "when" => "never" }, { "when" => "always" }],
|
|
288
|
+
"stage" => "build",
|
|
289
|
+
"script" => ["/usr/bin/gem-compile.sh"],
|
|
290
|
+
},
|
|
291
|
+
}
|
|
292
|
+
create_file File.join(atom_root, ".gitlab-ci.yml"), gitlab_ci.to_yaml
|
|
293
|
+
end
|
|
294
|
+
|
|
295
|
+
def setup_gemfile
|
|
296
|
+
gemfile_addition = +"\ngem 'pg'\n"
|
|
297
|
+
gemfile_addition << "gem 'model_driven_api', '#{MODEL_DRIVEN_API_VERSION}'\n" \
|
|
298
|
+
"gem 'thecore_ui_rails_admin', '#{THECORE_UI_RAILS_ADMIN_VERSION}'\n" if @include_api_admin_deps
|
|
299
|
+
append_to_file File.join(atom_root, "Gemfile"), gemfile_addition
|
|
300
|
+
|
|
301
|
+
return unless @include_api_admin_deps
|
|
302
|
+
|
|
303
|
+
append_to_file File.join(atom_root, "lib", "#{file_name}.rb"),
|
|
304
|
+
"\nrequire 'model_driven_api'\nrequire 'thecore_ui_rails_admin'\n"
|
|
305
|
+
end
|
|
306
|
+
|
|
307
|
+
# One read, one write, one pass of in-memory substitutions - not createATOM.js's
|
|
308
|
+
# own blind "rewrite every line, branching on substring" approach (the current
|
|
309
|
+
# `rails plugin new --full` gemspec template, verified directly against a real
|
|
310
|
+
# generation rather than assumed from the JS original, has moved on
|
|
311
|
+
# significantly: new `homepage_uri`/`license` lines, different summary/
|
|
312
|
+
# description wording - a full-file line-by-line port would silently stop
|
|
313
|
+
# matching several of these fields), and not 8 separate #gsub_file calls
|
|
314
|
+
# either (each its own full read-modify-write cycle against the same small
|
|
315
|
+
# file - wasted I/O for no benefit). One deliberate correctness fix over the
|
|
316
|
+
# original along the way: the JS's `.add_dependency` branch *replaces* the
|
|
317
|
+
# whole line, which happens to be `spec.add_dependency "rails", ...` in
|
|
318
|
+
# current Rails - silently dropping the gem's own Rails dependency entirely.
|
|
319
|
+
# This appends the two Thecore dependencies right after that line instead of
|
|
320
|
+
# replacing it.
|
|
321
|
+
def setup_gemspec
|
|
322
|
+
gemspec_path = File.join(atom_root, "#{file_name}.gemspec")
|
|
323
|
+
content = File.read(File.join(destination_root, gemspec_path))
|
|
324
|
+
|
|
325
|
+
content = content.sub(/^(\s*spec\.add_dependency\s+["']rails["'].*)$/) do
|
|
326
|
+
next Regexp.last_match(1) unless @include_api_admin_deps
|
|
327
|
+
|
|
328
|
+
"#{Regexp.last_match(1)}\n spec.add_dependency \"model_driven_api\", \"#{MODEL_DRIVEN_API_VERSION}\"\n" \
|
|
329
|
+
" spec.add_dependency \"thecore_ui_rails_admin\", \"#{THECORE_UI_RAILS_ADMIN_VERSION}\""
|
|
330
|
+
end
|
|
331
|
+
content = content.sub(/^\s*spec\.authors\s*=.*$/, " spec.authors = [#{@author.to_s.inspect}]")
|
|
332
|
+
content = content.sub(/^\s*spec\.email\s*=.*$/, " spec.email = [#{@email.to_s.inspect}]")
|
|
333
|
+
content = content.sub(/^\s*spec\.homepage\s*=.*$/, " spec.homepage = #{@url.to_s.inspect}")
|
|
334
|
+
content = content.sub(/^\s*spec\.summary\s*=.*$/, " spec.summary = #{@summary.to_s.inspect}")
|
|
335
|
+
content = content.sub(/^\s*spec\.description\s*=.*$/, " spec.description = #{@description.to_s.inspect}")
|
|
336
|
+
content = content.sub(/^\s*spec\.metadata\["allowed_push_host"\]\s*=.*$/,
|
|
337
|
+
' spec.metadata["allowed_push_host"] = "https://rubygems.org"')
|
|
338
|
+
content = content.sub(/^\s*spec\.metadata\["source_code_uri"\]\s*=.*$/,
|
|
339
|
+
' spec.metadata["source_code_uri"] = spec.homepage')
|
|
340
|
+
content = content.sub(/^\s*spec\.metadata\["changelog_uri"\]\s*=.*$/,
|
|
341
|
+
' spec.metadata["changelog_uri"] = "#{spec.homepage}/blob/master/CHANGELOG.md"')
|
|
342
|
+
|
|
343
|
+
create_file gemspec_path, content, force: true
|
|
344
|
+
end
|
|
345
|
+
|
|
346
|
+
# Completes this generator's scope (thecore_generators#22, ADR 0006): fetches
|
|
347
|
+
# thecore's own samples/ATOM_CLAUDE.md and writes it as the new ATOM's
|
|
348
|
+
# CLAUDE.md, via the shared Thecore::Generators::SampleFetcher - the same
|
|
349
|
+
# THECORE_SAMPLES_SOURCE mechanism the App template's own (separate, still
|
|
350
|
+
# independent - see sample_fetcher.rb's own header for why) asset fetch
|
|
351
|
+
# already established.
|
|
352
|
+
#
|
|
353
|
+
# A fetch failure here raises SystemExit (via Kernel#abort), not Thor::Error
|
|
354
|
+
# like every guard method above - a deliberate inconsistency, not an
|
|
355
|
+
# oversight: it matches the App template's own established convention for
|
|
356
|
+
# this exact class of failure (an external, this-run-only fetch, as opposed
|
|
357
|
+
# to a validation the generator could have caught before doing any real
|
|
358
|
+
# work), and the ticket's own acceptance criteria asks for "fail-fast abort,"
|
|
359
|
+
# not a Thor::Error. A failure here does leave the partially-generated
|
|
360
|
+
# vendor/submodules/<name> directory behind (rails plugin new/the Gemfile/
|
|
361
|
+
# gemspec edits already succeeded) - #ensure_atom_does_not_already_exist!
|
|
362
|
+
# then refuses a same-named retry until it's removed by hand; the abort
|
|
363
|
+
# message says so.
|
|
364
|
+
#
|
|
365
|
+
# NOTE: like the App template's own fetch of thecore/samples/CLAUDE.md
|
|
366
|
+
# before it, this 404s against the real default GitHub URL until thecore's
|
|
367
|
+
# `master` actually carries the commit that added samples/ATOM_CLAUDE.md
|
|
368
|
+
# (thecore#18) - as of this gem's 3.11.0 release that commit exists only in
|
|
369
|
+
# a local `thecore` checkout, not yet pushed. Same operational sequencing
|
|
370
|
+
# note as the App template's own CLAUDE.md section: push `thecore` before
|
|
371
|
+
# relying on the default in production.
|
|
372
|
+
def fetch_claude_md
|
|
373
|
+
Thecore::Generators::SampleFetcher.fetch_thecore_sample(
|
|
374
|
+
self, "ATOM_CLAUDE.md", File.join(atom_root, "CLAUDE.md"), label: "the thecore:atom generator"
|
|
375
|
+
)
|
|
376
|
+
end
|
|
377
|
+
|
|
378
|
+
# `-fG` (`rails plugin new`'s own force+skip-git flags) means no git repo and no
|
|
379
|
+
# .gitignore exist yet at this point - a deliberate gap in createATOM.js this
|
|
380
|
+
# ticket narrows, not fully closes (ADR 0006): a local, safe `git init` +
|
|
381
|
+
# initial commit, but remote creation and `git submodule add` stay a logged,
|
|
382
|
+
# human-run follow-up rather than something this generator automates. No
|
|
383
|
+
# .gitignore is written (out of this ticket's scope - see ADR 0006/the ticket's
|
|
384
|
+
# own acceptance criteria, which doesn't list one): verified directly that a
|
|
385
|
+
# fresh `rails plugin new --full` output has no log/tmp/sqlite artifacts yet to
|
|
386
|
+
# need ignoring - nothing has been bundled or run against the dummy app at this
|
|
387
|
+
# point, so the initial commit is clean regardless.
|
|
388
|
+
def git_init_and_commit
|
|
389
|
+
atom_path = File.join(destination_root, atom_root)
|
|
390
|
+
committed = inside(atom_root) do
|
|
391
|
+
run("git init -q -b master", abort_on_failure: true)
|
|
392
|
+
run("git add -A", abort_on_failure: true)
|
|
393
|
+
# Not abort_on_failure: a freshly-generated tree always has something to
|
|
394
|
+
# commit in normal use, but `git commit` failing (e.g. "nothing to
|
|
395
|
+
# commit", however that state arose) shouldn't kill the whole process via
|
|
396
|
+
# a raw, unexplained Kernel#abort when every file this generator actually
|
|
397
|
+
# promises has already been written successfully by this point. The
|
|
398
|
+
# result is still checked below, so a real failure changes what gets
|
|
399
|
+
# logged rather than being silently treated as success.
|
|
400
|
+
run(
|
|
401
|
+
"git -c user.name=#{Shellwords.escape(@author)} -c user.email=#{Shellwords.escape(@email)} " \
|
|
402
|
+
'commit -q -m "Initial commit"',
|
|
403
|
+
abort_on_failure: false
|
|
404
|
+
)
|
|
405
|
+
end
|
|
406
|
+
|
|
407
|
+
if committed
|
|
408
|
+
say_status :next_steps, <<~MSG.strip, :yellow
|
|
409
|
+
#{file_name} is git-initialized locally with one commit, but has no remote yet. To finish wiring it in:
|
|
410
|
+
1. Create a repository for it on the git host of your choice (GitHub, GitLab, ...)
|
|
411
|
+
2. cd #{atom_path} && git remote add origin <remote-url> && git push -u origin master
|
|
412
|
+
3. From this app's root: git submodule add <remote-url> vendor/submodules/#{file_name}
|
|
413
|
+
MSG
|
|
414
|
+
else
|
|
415
|
+
say_status :warning, <<~MSG.strip, :red
|
|
416
|
+
#{file_name} was git-initialized, but `git commit` did not succeed - it has no commit
|
|
417
|
+
yet. Check the output above, commit by hand once resolved, then follow the usual
|
|
418
|
+
steps to create a remote and `git submodule add` it into this app.
|
|
419
|
+
MSG
|
|
420
|
+
end
|
|
421
|
+
end
|
|
422
|
+
|
|
423
|
+
# Guards against the same name-collision scenario #ensure_atom_does_not_already_exist!
|
|
424
|
+
# protects the ATOM directory itself from: a host Gemfile that already
|
|
425
|
+
# declares a same-named gem (real in this very host app - `mytask` is
|
|
426
|
+
# resolved from a gem server today) would otherwise get a second, conflicting
|
|
427
|
+
# `gem "mytask", path: ...` line appended, and the next `bundle install`
|
|
428
|
+
# fails outright ("You cannot specify the same gem twice"). In the normal
|
|
429
|
+
# case (no prior entry) this is unreachable in practice anyway, since
|
|
430
|
+
# #ensure_atom_does_not_already_exist! already refuses a name whose
|
|
431
|
+
# vendor/submodules/<name> directory exists - kept as its own explicit check
|
|
432
|
+
# since a Gemfile entry and a vendor/submodules directory are two independent
|
|
433
|
+
# pieces of state that could in principle drift apart.
|
|
434
|
+
def add_gem_to_host_gemfile
|
|
435
|
+
gemfile_path = File.join(destination_root, "Gemfile")
|
|
436
|
+
if File.exist?(gemfile_path) && File.read(gemfile_path).match?(/^\s*gem\s+["']#{Regexp.escape(file_name)}["']/)
|
|
437
|
+
say_status :skip, "Gemfile already declares '#{file_name}' - not adding a second entry", :yellow
|
|
438
|
+
return
|
|
439
|
+
end
|
|
440
|
+
|
|
441
|
+
gem file_name, path: "vendor/submodules/#{file_name}"
|
|
442
|
+
end
|
|
443
|
+
|
|
444
|
+
private
|
|
445
|
+
|
|
446
|
+
def atom_root
|
|
447
|
+
File.join("vendor", "submodules", file_name)
|
|
448
|
+
end
|
|
449
|
+
|
|
450
|
+
# True whenever prompting for input isn't viable: --non-interactive was
|
|
451
|
+
# passed explicitly, or (via the shared Thecore::Generators::TtyDetection,
|
|
452
|
+
# also used by AssociationWiring's own `interactive_association_prompt?`)
|
|
453
|
+
# there's no real TTY behind stdin/stdout at all - a CI runner or a
|
|
454
|
+
# shelled-out child process that simply forgot the flag. Without this,
|
|
455
|
+
# #required_field's own `ask`-in-a-loop would spin forever re-prompting a
|
|
456
|
+
# stream that can never supply input, since a closed/EOF stdin makes Thor's
|
|
457
|
+
# `ask` return nil immediately.
|
|
458
|
+
def effectively_non_interactive?
|
|
459
|
+
options[:non_interactive] || !Thecore::Generators::TtyDetection.real_tty?
|
|
460
|
+
end
|
|
461
|
+
|
|
462
|
+
# Interactive: loop with Thor's own `ask` until the validator passes, echoing
|
|
463
|
+
# the same "not valid, try again" wording createATOM.js's input boxes used.
|
|
464
|
+
# Non-interactive (explicit or TTY-detected): already validated present by
|
|
465
|
+
# #validate_non_interactive_options! (a task method that always runs first),
|
|
466
|
+
# so this simply reads the option.
|
|
467
|
+
def required_field(key, prompt_hint)
|
|
468
|
+
return options[key] if effectively_non_interactive?
|
|
469
|
+
|
|
470
|
+
validate = REQUIRED_STRING_FIELDS.fetch(key)
|
|
471
|
+
loop do
|
|
472
|
+
value = ask("Enter #{prompt_hint}:")
|
|
473
|
+
return value if validate.call(value)
|
|
474
|
+
|
|
475
|
+
say_status :error, "The #{key} is not valid. Please try again.", :red
|
|
476
|
+
end
|
|
477
|
+
end
|
|
478
|
+
|
|
479
|
+
end
|
|
480
|
+
end
|
|
481
|
+
end
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
module Abilities
|
|
2
|
+
class <%= class_name %>
|
|
3
|
+
include CanCan::Ability
|
|
4
|
+
def initialize user
|
|
5
|
+
if user.present?
|
|
6
|
+
# Users' abilities
|
|
7
|
+
# Example: can :read, ModelName
|
|
8
|
+
# Example: can [:read, :create], ModelName
|
|
9
|
+
if user.admin?
|
|
10
|
+
# Admins' abilities
|
|
11
|
+
# Example: can :manage, :all
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
end
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
puts "Seeding Data into DB from <%= file_name %>"
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
require "rails/generators/named_base"
|
|
2
|
+
require "generators/thecore/atom_aware"
|
|
3
|
+
require "generators/thecore/companion_files"
|
|
4
|
+
require "generators/thecore/action_companion"
|
|
5
|
+
|
|
6
|
+
module Thecore
|
|
7
|
+
module Generators
|
|
8
|
+
# `rails generate thecore:collection_action NAME` (thecore_generators#21,
|
|
9
|
+
# per ADR 0006 in the thecore repo) — the third sibling to
|
|
10
|
+
# RootActionGenerator/MemberActionGenerator. Unlike those two, there is
|
|
11
|
+
# no prior thecore_code_extension JS command this ports (no
|
|
12
|
+
# `addCollectionAction.js` ever existed — collection_actions were only
|
|
13
|
+
# ever audited by check_practices, never generated), so its own
|
|
14
|
+
# `templates/action.rb.tt` deliberately mirrors RootActionGenerator's
|
|
15
|
+
# simplicity (a minimal GET/JSON example with an ActivityLogChannel
|
|
16
|
+
# broadcast) rather than the real, more complex hand-written
|
|
17
|
+
# `save_filters.rb`/`load_filters.rb` pattern already living in
|
|
18
|
+
# thecore_ui_rails_admin — a generator's starter template exists to be
|
|
19
|
+
# customized from a simple base, not to demonstrate every RailsAdmin
|
|
20
|
+
# :collection feature.
|
|
21
|
+
#
|
|
22
|
+
# Structurally identical to RootActionGenerator/MemberActionGenerator:
|
|
23
|
+
# same three includes, same thin task-method sequence (see
|
|
24
|
+
# ActionCompanion's own comment for why those can't be shared further),
|
|
25
|
+
# only `action_kind` and this class's own templates differ. No changes
|
|
26
|
+
# needed anywhere in AtomAware/CompanionFiles/ActionCompanion —
|
|
27
|
+
# `action_kind "collection_action"` alone is enough for placement
|
|
28
|
+
# (lib/collection_actions or config/collection_actions),
|
|
29
|
+
# pluralization, and validation wording to fall out correctly.
|
|
30
|
+
class CollectionActionGenerator < Rails::Generators::NamedBase
|
|
31
|
+
include Thecore::Generators::AtomAware
|
|
32
|
+
include Thecore::Generators::CompanionFiles
|
|
33
|
+
include Thecore::Generators::ActionCompanion
|
|
34
|
+
|
|
35
|
+
action_kind "collection_action"
|
|
36
|
+
|
|
37
|
+
source_root File.expand_path("templates", __dir__)
|
|
38
|
+
|
|
39
|
+
def validate_action_name!
|
|
40
|
+
validate_action_name_for_kind!
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def create_action_file
|
|
44
|
+
template "action.rb.tt", action_file_path
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def create_view_js_scss_companions
|
|
48
|
+
render_view_js_scss_companions!
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def add_after_initialize_require
|
|
52
|
+
ensure_after_initialize_require!(require_line)
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def add_assets_precompile_line
|
|
56
|
+
ensure_assets_precompile_line!(assets_precompile_line)
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def add_locale_entries
|
|
60
|
+
write_action_locale_entries!(file_name, title_case_name)
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
end
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
<%%= stylesheet_link_tag 'rails_admin/actions/<%= file_name %>' %>
|
|
2
|
+
<div class="card mb-3">
|
|
3
|
+
<div class="card-body">
|
|
4
|
+
<div class="response <%= file_name %>-response" id="<%= file_name %>-response">
|
|
5
|
+
</div>
|
|
6
|
+
<div class="loader d-none" id="<%= file_name %>-loader">
|
|
7
|
+
<div class="double-bounce1"></div>
|
|
8
|
+
<div class="double-bounce2"></div>
|
|
9
|
+
</div>
|
|
10
|
+
</div>
|
|
11
|
+
</div>
|
|
12
|
+
<button class="btn btn-primary" id="<%= file_name %>-id" data-url="<%%= rails_admin.<%= file_name %>_path %>">Click me</button>
|
|
13
|
+
<%%= javascript_include_tag "rails_admin/actions/<%= file_name %>" %>
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
var <%= action_name_camel_case %>Cable = null;
|
|
2
|
+
// If the <%= action_name_camel_case %>Function is already defined, then don't redefine it and don't attach it to the eventListener
|
|
3
|
+
if (typeof <%= action_name_camel_case %>Function !== 'function') {
|
|
4
|
+
function <%= action_name_camel_case %>Function(event) {
|
|
5
|
+
console.log('Hello from <%= file_name %>', event);
|
|
6
|
+
// Action Cable WebSocket connection only if <%= action_name_camel_case %>Cable is not already defined and valid
|
|
7
|
+
if (typeof <%= action_name_camel_case %>Cable !== 'object' || <%= action_name_camel_case %>Cable === null) {
|
|
8
|
+
<%= action_name_camel_case %>Cable = App.cable.subscriptions.create("ActivityLogChannel", {
|
|
9
|
+
connected() {
|
|
10
|
+
console.log("Connected to the channel:", this);
|
|
11
|
+
this.send({ message: '<%= file_name %> Client is connected', topic: "<%= file_name %>", namespace: "subscriptions" });
|
|
12
|
+
},
|
|
13
|
+
disconnected() {
|
|
14
|
+
console.log("<%= file_name %> Client Disconnected");
|
|
15
|
+
},
|
|
16
|
+
received(data) {
|
|
17
|
+
if(data["topic"] == "<%= file_name %>") {
|
|
18
|
+
console.log("<%= file_name %>", data);
|
|
19
|
+
document.getElementById('<%= file_name %>-response').innerHTML = data["message"];
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
});
|
|
23
|
+
}
|
|
24
|
+
// Send a message to the server
|
|
25
|
+
<%= action_name_camel_case %>Cable.send({ message: '<%= file_name %> Client is sending a message', topic: "<%= file_name %>", namespace: "subscriptions" });
|
|
26
|
+
// Attach a click event listener to the button which sends a fetch GET request and shows the response.
|
|
27
|
+
// The URL is read from the data-url attribute to avoid ERB interpolation in plain .js files.
|
|
28
|
+
document.getElementById('<%= file_name %>-id').addEventListener('click', function() {
|
|
29
|
+
var url = this.dataset.url;
|
|
30
|
+
document.getElementById('<%= file_name %>-loader').classList.remove('d-none');
|
|
31
|
+
fetch(url, { headers: { 'Accept': 'application/json' } })
|
|
32
|
+
.then(response => response.json())
|
|
33
|
+
.then(data => {
|
|
34
|
+
console.log(data);
|
|
35
|
+
document.getElementById('<%= file_name %>-response').innerHTML = data.message;
|
|
36
|
+
document.getElementById('<%= file_name %>-loader').classList.add('d-none');
|
|
37
|
+
});
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
// Attach the function to the eventListener
|
|
42
|
+
document.addEventListener('turbo:load', <%= action_name_camel_case %>Function);
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
RailsAdmin::Config::Actions.add_action "<%= file_name %>", :base, :collection do
|
|
2
|
+
# show_in_sidebar/show_in_navigation/breadcrumb_parent are deliberately not
|
|
3
|
+
# overridden here (unlike a root action) - RailsAdmin's own :collection
|
|
4
|
+
# defaults already do the right thing: sidebar-visible, not top-nav, and
|
|
5
|
+
# breadcrumbing back to the model's index page.
|
|
6
|
+
# This ensures the action only shows up for authorized users
|
|
7
|
+
visible? authorized?
|
|
8
|
+
# Have a look at https://fontawesome.com/v5/search for available icons
|
|
9
|
+
link_icon 'fas fa-file'
|
|
10
|
+
# The controller which will be used to compute the action and the REST verbs it will respond to
|
|
11
|
+
http_methods [:get]
|
|
12
|
+
# Adding the controller which is needed to compute calls from the ui
|
|
13
|
+
# This is a collection action: it runs against the whole model index (all
|
|
14
|
+
# records), not a single record (member) or globally (root) - @abstract_model
|
|
15
|
+
# is available here to scope the collection this action operates on.
|
|
16
|
+
controller do
|
|
17
|
+
proc do # This is needed because we need that this code is re-evaluated each time is called
|
|
18
|
+
if request.format.json?
|
|
19
|
+
# This is the code that is executed when the action is called
|
|
20
|
+
# It is executed in the context of the controller
|
|
21
|
+
# So you can access all the controller methods
|
|
22
|
+
# and instance variables
|
|
23
|
+
status = 200
|
|
24
|
+
message = "Hello World!"
|
|
25
|
+
# Note: ActivityLogChannel is expected to re-broadcast messages from the "messages" channel
|
|
26
|
+
ActionCable.server.broadcast("messages", { topic: :<%= file_name %>, status: status, message: message})
|
|
27
|
+
render json: {message: message}.to_json, status: status
|
|
28
|
+
else
|
|
29
|
+
# Renders the action.html.erb view for browser requests (HTML format)
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|