tsykvas_rails_template 0.1.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 +7 -0
- data/CHANGELOG.md +200 -0
- data/CODE_OF_CONDUCT.md +10 -0
- data/LICENSE.txt +21 -0
- data/README.md +589 -0
- data/Rakefile +17 -0
- data/lib/generators/tsykvas_rails_template/companions/companions_generator.rb +273 -0
- data/lib/generators/tsykvas_rails_template/concept/concept_generator.rb +145 -0
- data/lib/generators/tsykvas_rails_template/concept/templates/component/edit.html.slim.tt +5 -0
- data/lib/generators/tsykvas_rails_template/concept/templates/component/edit.rb.tt +11 -0
- data/lib/generators/tsykvas_rails_template/concept/templates/component/index.html.slim.tt +5 -0
- data/lib/generators/tsykvas_rails_template/concept/templates/component/index.rb.tt +11 -0
- data/lib/generators/tsykvas_rails_template/concept/templates/component/new.html.slim.tt +5 -0
- data/lib/generators/tsykvas_rails_template/concept/templates/component/new.rb.tt +11 -0
- data/lib/generators/tsykvas_rails_template/concept/templates/component/show.html.slim.tt +4 -0
- data/lib/generators/tsykvas_rails_template/concept/templates/component/show.rb.tt +11 -0
- data/lib/generators/tsykvas_rails_template/concept/templates/controller.rb.tt +45 -0
- data/lib/generators/tsykvas_rails_template/concept/templates/operation/create.rb.tt +31 -0
- data/lib/generators/tsykvas_rails_template/concept/templates/operation/destroy.rb.tt +13 -0
- data/lib/generators/tsykvas_rails_template/concept/templates/operation/edit.rb.tt +10 -0
- data/lib/generators/tsykvas_rails_template/concept/templates/operation/index.rb.tt +9 -0
- data/lib/generators/tsykvas_rails_template/concept/templates/operation/new.rb.tt +10 -0
- data/lib/generators/tsykvas_rails_template/concept/templates/operation/show.rb.tt +10 -0
- data/lib/generators/tsykvas_rails_template/concept/templates/operation/update.rb.tt +31 -0
- data/lib/generators/tsykvas_rails_template/install/bootstrap_installer.rb +225 -0
- data/lib/generators/tsykvas_rails_template/install/install_generator.rb +298 -0
- data/lib/generators/tsykvas_rails_template/install/templates/.claude/agents/buddy.md +157 -0
- data/lib/generators/tsykvas_rails_template/install/templates/.claude/agents/code-reviewer.md +117 -0
- data/lib/generators/tsykvas_rails_template/install/templates/.claude/agents/security-reviewer.md +113 -0
- data/lib/generators/tsykvas_rails_template/install/templates/.claude/agents/tech-lead.md +150 -0
- data/lib/generators/tsykvas_rails_template/install/templates/.claude/commands/check.md +51 -0
- data/lib/generators/tsykvas_rails_template/install/templates/.claude/commands/code-review.md +60 -0
- data/lib/generators/tsykvas_rails_template/install/templates/.claude/commands/docs-create.md +102 -0
- data/lib/generators/tsykvas_rails_template/install/templates/.claude/commands/pr-review.md +81 -0
- data/lib/generators/tsykvas_rails_template/install/templates/.claude/commands/pushit.md +160 -0
- data/lib/generators/tsykvas_rails_template/install/templates/.claude/commands/refactor.md +132 -0
- data/lib/generators/tsykvas_rails_template/install/templates/.claude/commands/task-sum.md +47 -0
- data/lib/generators/tsykvas_rails_template/install/templates/.claude/commands/tests.md +67 -0
- data/lib/generators/tsykvas_rails_template/install/templates/.claude/commands/tsykvas-claude.md +262 -0
- data/lib/generators/tsykvas_rails_template/install/templates/.claude/commands/update-docs.md +78 -0
- data/lib/generators/tsykvas_rails_template/install/templates/.claude/commands/update-rules.md +102 -0
- data/lib/generators/tsykvas_rails_template/install/templates/.claude/commands/update-tests.md +135 -0
- data/lib/generators/tsykvas_rails_template/install/templates/.claude/docs/architecture.md +315 -0
- data/lib/generators/tsykvas_rails_template/install/templates/.claude/docs/authentication.md +96 -0
- data/lib/generators/tsykvas_rails_template/install/templates/.claude/docs/background-jobs.md +135 -0
- data/lib/generators/tsykvas_rails_template/install/templates/.claude/docs/code-style.md +101 -0
- data/lib/generators/tsykvas_rails_template/install/templates/.claude/docs/commands.md +34 -0
- data/lib/generators/tsykvas_rails_template/install/templates/.claude/docs/companions.md +128 -0
- data/lib/generators/tsykvas_rails_template/install/templates/.claude/docs/concepts-refactoring.md +194 -0
- data/lib/generators/tsykvas_rails_template/install/templates/.claude/docs/database.md +135 -0
- data/lib/generators/tsykvas_rails_template/install/templates/.claude/docs/deployment.md +138 -0
- data/lib/generators/tsykvas_rails_template/install/templates/.claude/docs/design-system.md +322 -0
- data/lib/generators/tsykvas_rails_template/install/templates/.claude/docs/documentation.md +89 -0
- data/lib/generators/tsykvas_rails_template/install/templates/.claude/docs/forms.md +174 -0
- data/lib/generators/tsykvas_rails_template/install/templates/.claude/docs/i18n.md +165 -0
- data/lib/generators/tsykvas_rails_template/install/templates/.claude/docs/routing-and-namespaces.md +114 -0
- data/lib/generators/tsykvas_rails_template/install/templates/.claude/docs/security.md +122 -0
- data/lib/generators/tsykvas_rails_template/install/templates/.claude/docs/stimulus-controllers.md +166 -0
- data/lib/generators/tsykvas_rails_template/install/templates/.claude/docs/testing-examples.md +180 -0
- data/lib/generators/tsykvas_rails_template/install/templates/.claude/docs/testing.md +117 -0
- data/lib/generators/tsykvas_rails_template/install/templates/.claude/docs/tsykvas_rails_template.md +280 -0
- data/lib/generators/tsykvas_rails_template/install/templates/.claude/docs/ui-components.md +196 -0
- data/lib/generators/tsykvas_rails_template/install/templates/CLAUDE.md.tt +81 -0
- data/lib/generators/tsykvas_rails_template/install/templates/app/concepts/base/component/base.rb +6 -0
- data/lib/generators/tsykvas_rails_template/install/templates/app/concepts/base/operation/base.rb +124 -0
- data/lib/generators/tsykvas_rails_template/install/templates/app/concepts/base/operation/result.rb +56 -0
- data/lib/generators/tsykvas_rails_template/install/templates/app/concepts/home/component/index.html.slim +49 -0
- data/lib/generators/tsykvas_rails_template/install/templates/app/concepts/home/component/index.rb +11 -0
- data/lib/generators/tsykvas_rails_template/install/templates/app/concepts/home/operation/index.rb +17 -0
- data/lib/generators/tsykvas_rails_template/install/templates/app/controllers/concerns/operations_methods.rb +148 -0
- data/lib/generators/tsykvas_rails_template/install/templates/app/controllers/home_controller.rb +10 -0
- data/lib/generators/tsykvas_rails_template/install/templates/app/policies/application_policy.rb +33 -0
- data/lib/generators/tsykvas_rails_template/install/templates/app/policies/home_policy.rb +8 -0
- data/lib/tasks/tsykvas.rake +11 -0
- data/lib/tsykvas_rails_template/probe.rb +236 -0
- data/lib/tsykvas_rails_template/railtie.rb +13 -0
- data/lib/tsykvas_rails_template/version.rb +5 -0
- data/lib/tsykvas_rails_template.rb +18 -0
- metadata +183 -0
|
@@ -0,0 +1,273 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "rails/generators/base"
|
|
4
|
+
require "tsykvas_rails_template/probe"
|
|
5
|
+
|
|
6
|
+
module TsykvasRailsTemplate
|
|
7
|
+
module Generators
|
|
8
|
+
# Adds the recommended companion gems used across the author's reference
|
|
9
|
+
# projects (sport / planner / esl). Runs `bundle install` and the per-gem
|
|
10
|
+
# `:install` sub-generators (no User model is generated for Devise).
|
|
11
|
+
#
|
|
12
|
+
# Designed to be idempotent: re-running the generator does not duplicate
|
|
13
|
+
# Gemfile entries, re-run sub-generators, or re-inject configuration.
|
|
14
|
+
class CompanionsGenerator < ::Rails::Generators::Base
|
|
15
|
+
source_root File.expand_path("templates", __dir__)
|
|
16
|
+
|
|
17
|
+
desc <<~DESC
|
|
18
|
+
Add and install the recommended companion gems for tsykvas_rails_template.
|
|
19
|
+
|
|
20
|
+
Default groups: auth (devise + omniauth-csrf), forms (simple_form),
|
|
21
|
+
images (mini_magick), jobs-ui (mission_control-jobs, only if solid_queue),
|
|
22
|
+
test (rspec-rails + factory_bot_rails + shoulda-matchers + webmock + faker),
|
|
23
|
+
dev (dotenv-rails). Use --skip-* flags to opt out per group.
|
|
24
|
+
|
|
25
|
+
Devise's User model is NOT generated — run `rails g devise User` (or
|
|
26
|
+
whatever resource name fits your domain) yourself when ready.
|
|
27
|
+
DESC
|
|
28
|
+
|
|
29
|
+
class_option :skip_auth,
|
|
30
|
+
type: :boolean, default: false,
|
|
31
|
+
desc: "Don't add devise + omniauth-rails_csrf_protection"
|
|
32
|
+
class_option :skip_forms,
|
|
33
|
+
type: :boolean, default: false,
|
|
34
|
+
desc: "Don't add simple_form"
|
|
35
|
+
class_option :skip_images,
|
|
36
|
+
type: :boolean, default: false,
|
|
37
|
+
desc: "Don't add mini_magick"
|
|
38
|
+
class_option :skip_jobs_ui,
|
|
39
|
+
type: :boolean, default: false,
|
|
40
|
+
desc: "Don't add mission_control-jobs (or its mount)"
|
|
41
|
+
class_option :skip_test,
|
|
42
|
+
type: :boolean, default: false,
|
|
43
|
+
desc: "Don't add the rspec/factory_bot/shoulda/webmock/faker stack"
|
|
44
|
+
class_option :skip_dev,
|
|
45
|
+
type: :boolean, default: false,
|
|
46
|
+
desc: "Don't add dotenv-rails"
|
|
47
|
+
class_option :skip_bundle,
|
|
48
|
+
type: :boolean, default: false,
|
|
49
|
+
desc: "Don't run `bundle install` after editing Gemfile"
|
|
50
|
+
class_option :skip_post_install,
|
|
51
|
+
type: :boolean, default: false,
|
|
52
|
+
desc: "Don't run `:install` sub-generators or inject configs"
|
|
53
|
+
|
|
54
|
+
def add_top_level_gems
|
|
55
|
+
wanted = []
|
|
56
|
+
wanted += %w[devise omniauth-rails_csrf_protection] unless options[:skip_auth]
|
|
57
|
+
wanted << "simple_form" unless options[:skip_forms]
|
|
58
|
+
wanted << "mini_magick" unless options[:skip_images]
|
|
59
|
+
wanted << "mission_control-jobs" if !options[:skip_jobs_ui] && solid_queue_present?
|
|
60
|
+
|
|
61
|
+
wanted.each { |name| add_gem_if_missing(name) }
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def add_test_group_gems
|
|
65
|
+
return if options[:skip_test]
|
|
66
|
+
|
|
67
|
+
add_grouped_gems(%i[development test], %w[rspec-rails factory_bot_rails faker])
|
|
68
|
+
add_grouped_gems([:test], %w[shoulda-matchers webmock])
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def add_dev_group_gems
|
|
72
|
+
return if options[:skip_dev]
|
|
73
|
+
|
|
74
|
+
add_grouped_gems(%i[development test], %w[dotenv-rails])
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def run_bundle_install
|
|
78
|
+
return if options[:skip_bundle]
|
|
79
|
+
|
|
80
|
+
in_root do
|
|
81
|
+
Bundler.with_unbundled_env do
|
|
82
|
+
run "bundle install"
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
# Bump the companion gems we just added to their latest matching
|
|
88
|
+
# versions. `bundle install` is a no-op on re-runs where Gemfile.lock
|
|
89
|
+
# already pins older versions; `bundle update` lifts those pins within
|
|
90
|
+
# whatever constraint the Gemfile expresses (none, by default → latest).
|
|
91
|
+
def bundle_update_companions
|
|
92
|
+
return if options[:skip_bundle]
|
|
93
|
+
|
|
94
|
+
names = companion_gem_names
|
|
95
|
+
return if names.empty?
|
|
96
|
+
|
|
97
|
+
in_root do
|
|
98
|
+
Bundler.with_unbundled_env { run "bundle update #{names.join(" ")}" }
|
|
99
|
+
end
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
def run_devise_install
|
|
103
|
+
return if skip_post_install_for?(:auth)
|
|
104
|
+
return if File.exist?(destination_path("config/initializers/devise.rb"))
|
|
105
|
+
|
|
106
|
+
generate "devise:install"
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
def run_simple_form_install
|
|
110
|
+
return if skip_post_install_for?(:forms)
|
|
111
|
+
return if File.exist?(destination_path("config/initializers/simple_form.rb"))
|
|
112
|
+
|
|
113
|
+
flag = probe[:has_bootstrap] ? " --bootstrap" : ""
|
|
114
|
+
generate "simple_form:install#{flag}"
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
def run_rspec_install
|
|
118
|
+
return if skip_post_install_for?(:test)
|
|
119
|
+
return if File.exist?(destination_path("spec/rails_helper.rb"))
|
|
120
|
+
|
|
121
|
+
generate "rspec:install"
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
def configure_shoulda_matchers
|
|
125
|
+
return if skip_post_install_for?(:test)
|
|
126
|
+
return unless File.exist?(destination_path("spec/rails_helper.rb"))
|
|
127
|
+
return if File.read(destination_path("spec/rails_helper.rb")).include?("Shoulda::Matchers")
|
|
128
|
+
|
|
129
|
+
append_to_file "spec/rails_helper.rb", shoulda_config_block
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
def configure_webmock
|
|
133
|
+
return if skip_post_install_for?(:test)
|
|
134
|
+
return unless File.exist?(destination_path("spec/rails_helper.rb"))
|
|
135
|
+
return if File.read(destination_path("spec/rails_helper.rb")).include?("WebMock.disable_net_connect!")
|
|
136
|
+
|
|
137
|
+
append_to_file "spec/rails_helper.rb", webmock_config_block
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
def mount_mission_control_jobs
|
|
141
|
+
return if skip_post_install_for?(:jobs_ui)
|
|
142
|
+
return unless solid_queue_present?
|
|
143
|
+
return unless File.exist?(destination_path("config/routes.rb"))
|
|
144
|
+
return if File.read(destination_path("config/routes.rb")).include?("MissionControl::Jobs::Engine")
|
|
145
|
+
|
|
146
|
+
route mission_control_route_block
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
def add_dotenv_to_gitignore
|
|
150
|
+
return if skip_post_install_for?(:dev)
|
|
151
|
+
return unless File.exist?(destination_path(".gitignore"))
|
|
152
|
+
return if File.read(destination_path(".gitignore")).match?(/^\.env\b/)
|
|
153
|
+
|
|
154
|
+
append_to_file ".gitignore",
|
|
155
|
+
"\n# dotenv-rails (added by tsykvas_rails_template:companions)\n.env\n.env.*\n!.env.example\n"
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
def announce
|
|
159
|
+
say ""
|
|
160
|
+
say " Companions installed.", :green
|
|
161
|
+
say " Next: rails g devise User (run yourself when your user schema is ready)"
|
|
162
|
+
say " /jobs UI is mounted with admin-only constraint; needs User#admin?"
|
|
163
|
+
say " Image processing requires ImageMagick installed system-wide."
|
|
164
|
+
say ""
|
|
165
|
+
end
|
|
166
|
+
|
|
167
|
+
private
|
|
168
|
+
|
|
169
|
+
def destination_path(rel)
|
|
170
|
+
File.join(destination_root, rel)
|
|
171
|
+
end
|
|
172
|
+
|
|
173
|
+
def gemfile_path
|
|
174
|
+
destination_path("Gemfile")
|
|
175
|
+
end
|
|
176
|
+
|
|
177
|
+
def gemfile_content
|
|
178
|
+
@gemfile_content = nil if defined?(@gemfile_content_mtime) && @gemfile_content_mtime != File.mtime(gemfile_path)
|
|
179
|
+
return "" unless File.exist?(gemfile_path)
|
|
180
|
+
|
|
181
|
+
@gemfile_content_mtime = File.mtime(gemfile_path)
|
|
182
|
+
@gemfile_content ||= File.read(gemfile_path)
|
|
183
|
+
end
|
|
184
|
+
|
|
185
|
+
def gem_in_gemfile?(name)
|
|
186
|
+
gemfile_content.match?(/^\s*gem\s+['"]#{Regexp.escape(name)}['"]/)
|
|
187
|
+
end
|
|
188
|
+
|
|
189
|
+
def add_gem_if_missing(name, *args)
|
|
190
|
+
if gem_in_gemfile?(name)
|
|
191
|
+
say_status :exist, "gem '#{name}' already in Gemfile", :blue
|
|
192
|
+
return
|
|
193
|
+
end
|
|
194
|
+
|
|
195
|
+
gem(name, *args)
|
|
196
|
+
# Force re-read on next gem_in_gemfile? check.
|
|
197
|
+
@gemfile_content = nil
|
|
198
|
+
end
|
|
199
|
+
|
|
200
|
+
def add_grouped_gems(groups, names)
|
|
201
|
+
needed = names.reject { |n| gem_in_gemfile?(n) }
|
|
202
|
+
return if needed.empty?
|
|
203
|
+
|
|
204
|
+
gem_group(*groups) { needed.each { |n| gem n } }
|
|
205
|
+
@gemfile_content = nil
|
|
206
|
+
end
|
|
207
|
+
|
|
208
|
+
COMPANION_GROUPS = {
|
|
209
|
+
skip_auth: %w[devise omniauth-rails_csrf_protection],
|
|
210
|
+
skip_forms: %w[simple_form],
|
|
211
|
+
skip_images: %w[mini_magick],
|
|
212
|
+
skip_test: %w[rspec-rails factory_bot_rails faker shoulda-matchers webmock],
|
|
213
|
+
skip_dev: %w[dotenv-rails]
|
|
214
|
+
}.freeze
|
|
215
|
+
|
|
216
|
+
def companion_gem_names
|
|
217
|
+
names = COMPANION_GROUPS.flat_map { |opt, gems| options[opt] ? [] : gems }
|
|
218
|
+
names << "mission_control-jobs" if !options[:skip_jobs_ui] && solid_queue_present?
|
|
219
|
+
names.select { |n| gem_in_gemfile?(n) }
|
|
220
|
+
end
|
|
221
|
+
|
|
222
|
+
def probe
|
|
223
|
+
@probe ||= TsykvasRailsTemplate::Probe.run(root: destination_root)
|
|
224
|
+
end
|
|
225
|
+
|
|
226
|
+
def solid_queue_present?
|
|
227
|
+
probe[:background_jobs].include?(:solid_queue)
|
|
228
|
+
end
|
|
229
|
+
|
|
230
|
+
def skip_post_install_for?(group)
|
|
231
|
+
return true if options[:skip_post_install]
|
|
232
|
+
|
|
233
|
+
options["skip_#{group}".to_sym]
|
|
234
|
+
end
|
|
235
|
+
|
|
236
|
+
def shoulda_config_block
|
|
237
|
+
<<~RUBY
|
|
238
|
+
|
|
239
|
+
# shoulda-matchers (added by tsykvas_rails_template:companions)
|
|
240
|
+
Shoulda::Matchers.configure do |config|
|
|
241
|
+
config.integrate do |with|
|
|
242
|
+
with.test_framework :rspec
|
|
243
|
+
with.library :rails
|
|
244
|
+
end
|
|
245
|
+
end
|
|
246
|
+
RUBY
|
|
247
|
+
end
|
|
248
|
+
|
|
249
|
+
def webmock_config_block
|
|
250
|
+
<<~RUBY
|
|
251
|
+
|
|
252
|
+
# webmock (added by tsykvas_rails_template:companions)
|
|
253
|
+
require "webmock/rspec"
|
|
254
|
+
WebMock.disable_net_connect!(allow_localhost: true)
|
|
255
|
+
RUBY
|
|
256
|
+
end
|
|
257
|
+
|
|
258
|
+
def mission_control_route_block
|
|
259
|
+
<<~RUBY
|
|
260
|
+
# MissionControl::Jobs UI — admins only.
|
|
261
|
+
# Lambda runs per request, so missing User model at boot doesn't crash.
|
|
262
|
+
# Without User#admin? all /jobs requests return 404 (lock-by-default).
|
|
263
|
+
mount MissionControl::Jobs::Engine,
|
|
264
|
+
at: "/jobs",
|
|
265
|
+
constraints: ->(req) {
|
|
266
|
+
user = req.env["warden"]&.user
|
|
267
|
+
user.respond_to?(:admin?) && user.admin?
|
|
268
|
+
}
|
|
269
|
+
RUBY
|
|
270
|
+
end
|
|
271
|
+
end
|
|
272
|
+
end
|
|
273
|
+
end
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "rails/generators/base"
|
|
4
|
+
|
|
5
|
+
module TsykvasRailsTemplate
|
|
6
|
+
module Generators
|
|
7
|
+
class ConceptGenerator < ::Rails::Generators::Base
|
|
8
|
+
argument :concept_name,
|
|
9
|
+
type: :string,
|
|
10
|
+
banner: "ConceptName"
|
|
11
|
+
|
|
12
|
+
source_root File.expand_path("templates", __dir__)
|
|
13
|
+
|
|
14
|
+
desc <<~DESC
|
|
15
|
+
Scaffold a new concept under app/concepts/<path>/{operation,component}/.
|
|
16
|
+
|
|
17
|
+
Examples:
|
|
18
|
+
rails g tsykvas_rails_template:concept Crm::Property
|
|
19
|
+
rails g tsykvas_rails_template:concept Property --controller
|
|
20
|
+
rails g tsykvas_rails_template:concept Admin::User --actions index show
|
|
21
|
+
DESC
|
|
22
|
+
|
|
23
|
+
class_option :actions,
|
|
24
|
+
type: :array,
|
|
25
|
+
default: %w[index show new create edit update destroy],
|
|
26
|
+
desc: "Subset of CRUD actions to generate"
|
|
27
|
+
|
|
28
|
+
class_option :controller,
|
|
29
|
+
type: :boolean,
|
|
30
|
+
default: false,
|
|
31
|
+
desc: "Also generate a thin controller"
|
|
32
|
+
|
|
33
|
+
VALID_CONCEPT_NAME = %r{\A[A-Za-z][A-Za-z0-9]*(?:(?:::|/)[A-Za-z][A-Za-z0-9]*)*\z}
|
|
34
|
+
|
|
35
|
+
def validate_concept_name
|
|
36
|
+
name = concept_name.to_s.strip
|
|
37
|
+
|
|
38
|
+
if name.empty?
|
|
39
|
+
raise ::Thor::Error,
|
|
40
|
+
"concept name is required (e.g. 'Crm::Property' or 'crm/property')"
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
if name.start_with?("::") || name.start_with?("/")
|
|
44
|
+
raise ::Thor::Error,
|
|
45
|
+
"concept name '#{name}' must not start with '::' or '/'"
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
return if name.match?(VALID_CONCEPT_NAME)
|
|
49
|
+
|
|
50
|
+
raise ::Thor::Error,
|
|
51
|
+
"concept name '#{name}' has invalid characters " \
|
|
52
|
+
"(allowed: letters, digits, '::' or '/' separators; " \
|
|
53
|
+
"each segment must start with a letter)"
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def generate_operations
|
|
57
|
+
actions.each do |action|
|
|
58
|
+
template "operation/#{action}.rb.tt",
|
|
59
|
+
"app/concepts/#{path}/operation/#{action}.rb"
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def generate_components
|
|
64
|
+
component_actions.each do |action|
|
|
65
|
+
template "component/#{action}.rb.tt",
|
|
66
|
+
"app/concepts/#{path}/component/#{action}.rb"
|
|
67
|
+
template "component/#{action}.html.slim.tt",
|
|
68
|
+
"app/concepts/#{path}/component/#{action}.html.slim"
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def generate_controller
|
|
73
|
+
return unless options[:controller]
|
|
74
|
+
|
|
75
|
+
template "controller.rb.tt",
|
|
76
|
+
"app/controllers/#{controller_file_path}.rb"
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
def announce_next_steps
|
|
80
|
+
say ""
|
|
81
|
+
say " Concept #{class_name} scaffolded.", :green
|
|
82
|
+
say " - operations: app/concepts/#{path}/operation/"
|
|
83
|
+
say " - components: app/concepts/#{path}/component/"
|
|
84
|
+
say " - controller: app/controllers/#{controller_file_path}.rb" if options[:controller]
|
|
85
|
+
say " Add routes for #{plural_var.tr("_", " ")} in config/routes.rb."
|
|
86
|
+
say " Implement the policy at app/policies/#{singular_var}_policy.rb."
|
|
87
|
+
say ""
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
private
|
|
91
|
+
|
|
92
|
+
def actions
|
|
93
|
+
options[:actions]
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
def component_actions
|
|
97
|
+
actions & %w[index show new edit]
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
def class_name
|
|
101
|
+
@class_name ||= concept_name.split(%r{::|/}).reject(&:empty?).map(&:camelize).join("::")
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
def path
|
|
105
|
+
@path ||= class_name.gsub("::", "/").underscore
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
def singular_var
|
|
109
|
+
@singular_var ||= path.split("/").last
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
def plural_var
|
|
113
|
+
@plural_var ||= singular_var.pluralize
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
def resource_class
|
|
117
|
+
@resource_class ||= class_name.split("::").last
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
def i18n_key
|
|
121
|
+
@i18n_key ||= path.tr("/", ".")
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
def controller_class_name
|
|
125
|
+
parts = class_name.split("::")
|
|
126
|
+
parts[-1] = parts.last.pluralize
|
|
127
|
+
"#{parts.join("::")}Controller"
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
def controller_file_path
|
|
131
|
+
pieces = path.split("/")
|
|
132
|
+
pieces[-1] = pieces.last.pluralize
|
|
133
|
+
"#{pieces.join("/")}_controller"
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
def url_helper_singular
|
|
137
|
+
path.tr("/", "_")
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
def url_helper_plural
|
|
141
|
+
url_helper_singular.sub(/#{Regexp.escape(singular_var)}\z/, plural_var)
|
|
142
|
+
end
|
|
143
|
+
end
|
|
144
|
+
end
|
|
145
|
+
end
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
class <%= controller_class_name %> < ApplicationController
|
|
4
|
+
<% if actions.include?("index") -%>
|
|
5
|
+
def index
|
|
6
|
+
endpoint <%= class_name %>::Operation::Index, <%= class_name %>::Component::Index
|
|
7
|
+
end
|
|
8
|
+
|
|
9
|
+
<% end -%>
|
|
10
|
+
<% if actions.include?("show") -%>
|
|
11
|
+
def show
|
|
12
|
+
endpoint <%= class_name %>::Operation::Show, <%= class_name %>::Component::Show
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
<% end -%>
|
|
16
|
+
<% if actions.include?("new") -%>
|
|
17
|
+
def new
|
|
18
|
+
endpoint <%= class_name %>::Operation::New, <%= class_name %>::Component::New
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
<% end -%>
|
|
22
|
+
<% if actions.include?("create") -%>
|
|
23
|
+
def create
|
|
24
|
+
endpoint <%= class_name %>::Operation::Create, <%= class_name %>::Component::New
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
<% end -%>
|
|
28
|
+
<% if actions.include?("edit") -%>
|
|
29
|
+
def edit
|
|
30
|
+
endpoint <%= class_name %>::Operation::Edit, <%= class_name %>::Component::Edit
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
<% end -%>
|
|
34
|
+
<% if actions.include?("update") -%>
|
|
35
|
+
def update
|
|
36
|
+
endpoint <%= class_name %>::Operation::Update, <%= class_name %>::Component::Edit
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
<% end -%>
|
|
40
|
+
<% if actions.include?("destroy") -%>
|
|
41
|
+
def destroy
|
|
42
|
+
endpoint <%= class_name %>::Operation::Destroy
|
|
43
|
+
end
|
|
44
|
+
<% end -%>
|
|
45
|
+
end
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
class <%= class_name %>::Operation::Create < ::Base::Operation::Base
|
|
4
|
+
def perform!(params:, current_user:)
|
|
5
|
+
<%= singular_var %> = <%= resource_class %>.new
|
|
6
|
+
authorize! <%= singular_var %>, :create?
|
|
7
|
+
|
|
8
|
+
<%= singular_var %>.assign_attributes(<%= singular_var %>_params(params))
|
|
9
|
+
<%= singular_var %>.save!
|
|
10
|
+
|
|
11
|
+
self.model = <%= singular_var %>
|
|
12
|
+
self.redirect_path = "/<%= url_helper_plural.tr('_', '/') %>/#{<%= singular_var %>.id}"
|
|
13
|
+
notice(I18n.t("<%= i18n_key %>.create.success"))
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
private
|
|
17
|
+
|
|
18
|
+
# For complex forms (virtual attributes, sub-operation calls during
|
|
19
|
+
# assignment, multi-record submits) promote this into a
|
|
20
|
+
# <%= class_name %>::Form object. See .claude/docs/forms.md.
|
|
21
|
+
def <%= singular_var %>_params(params)
|
|
22
|
+
raise NotImplementedError, <<~MSG
|
|
23
|
+
Implement <%= class_name %>::Operation::Create#<%= singular_var %>_params.
|
|
24
|
+
|
|
25
|
+
params.require(:<%= singular_var %>).permit(:name, :description) # list real attributes
|
|
26
|
+
|
|
27
|
+
For complex forms, promote into a <%= class_name %>::Form object
|
|
28
|
+
instead. See .claude/docs/forms.md.
|
|
29
|
+
MSG
|
|
30
|
+
end
|
|
31
|
+
end
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
class <%= class_name %>::Operation::Destroy < ::Base::Operation::Base
|
|
4
|
+
def perform!(params:, current_user:)
|
|
5
|
+
<%= singular_var %> = <%= resource_class %>.find(params[:id])
|
|
6
|
+
authorize! <%= singular_var %>, :destroy?
|
|
7
|
+
<%= singular_var %>.destroy!
|
|
8
|
+
|
|
9
|
+
self.model = <%= singular_var %>
|
|
10
|
+
self.redirect_path = "/<%= url_helper_plural.tr('_', '/') %>"
|
|
11
|
+
notice(I18n.t("<%= i18n_key %>.destroy.success"))
|
|
12
|
+
end
|
|
13
|
+
end
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
class <%= class_name %>::Operation::Edit < ::Base::Operation::Base
|
|
4
|
+
def perform!(params:, current_user:)
|
|
5
|
+
<%= singular_var %> = <%= resource_class %>.find(params[:id])
|
|
6
|
+
authorize! <%= singular_var %>, :edit?
|
|
7
|
+
|
|
8
|
+
self.model = <%= singular_var %>
|
|
9
|
+
end
|
|
10
|
+
end
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
class <%= class_name %>::Operation::Index < ::Base::Operation::Base
|
|
4
|
+
def perform!(params:, current_user:)
|
|
5
|
+
<%= plural_var %> = policy_scope(<%= resource_class %>)
|
|
6
|
+
|
|
7
|
+
self.model = ::OpenStruct.new(<%= plural_var %>: <%= plural_var %>)
|
|
8
|
+
end
|
|
9
|
+
end
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
class <%= class_name %>::Operation::New < ::Base::Operation::Base
|
|
4
|
+
def perform!(params:, current_user:)
|
|
5
|
+
<%= singular_var %> = <%= resource_class %>.new
|
|
6
|
+
authorize! <%= singular_var %>, :new?
|
|
7
|
+
|
|
8
|
+
self.model = <%= singular_var %>
|
|
9
|
+
end
|
|
10
|
+
end
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
class <%= class_name %>::Operation::Show < ::Base::Operation::Base
|
|
4
|
+
def perform!(params:, current_user:)
|
|
5
|
+
<%= singular_var %> = <%= resource_class %>.find(params[:id])
|
|
6
|
+
authorize! <%= singular_var %>, :show?
|
|
7
|
+
|
|
8
|
+
self.model = <%= singular_var %>
|
|
9
|
+
end
|
|
10
|
+
end
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
class <%= class_name %>::Operation::Update < ::Base::Operation::Base
|
|
4
|
+
def perform!(params:, current_user:)
|
|
5
|
+
<%= singular_var %> = <%= resource_class %>.find(params[:id])
|
|
6
|
+
authorize! <%= singular_var %>, :update?
|
|
7
|
+
|
|
8
|
+
<%= singular_var %>.assign_attributes(<%= singular_var %>_params(params))
|
|
9
|
+
<%= singular_var %>.save!
|
|
10
|
+
|
|
11
|
+
self.model = <%= singular_var %>
|
|
12
|
+
self.redirect_path = "/<%= url_helper_plural.tr('_', '/') %>/#{<%= singular_var %>.id}"
|
|
13
|
+
notice(I18n.t("<%= i18n_key %>.update.success"))
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
private
|
|
17
|
+
|
|
18
|
+
# For complex forms (virtual attributes, sub-operation calls during
|
|
19
|
+
# assignment, multi-record submits) promote this into a
|
|
20
|
+
# <%= class_name %>::Form object. See .claude/docs/forms.md.
|
|
21
|
+
def <%= singular_var %>_params(params)
|
|
22
|
+
raise NotImplementedError, <<~MSG
|
|
23
|
+
Implement <%= class_name %>::Operation::Update#<%= singular_var %>_params.
|
|
24
|
+
|
|
25
|
+
params.require(:<%= singular_var %>).permit(:name, :description) # list real attributes
|
|
26
|
+
|
|
27
|
+
For complex forms, promote into a <%= class_name %>::Form object
|
|
28
|
+
instead. See .claude/docs/forms.md.
|
|
29
|
+
MSG
|
|
30
|
+
end
|
|
31
|
+
end
|