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.
Files changed (79) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +200 -0
  3. data/CODE_OF_CONDUCT.md +10 -0
  4. data/LICENSE.txt +21 -0
  5. data/README.md +589 -0
  6. data/Rakefile +17 -0
  7. data/lib/generators/tsykvas_rails_template/companions/companions_generator.rb +273 -0
  8. data/lib/generators/tsykvas_rails_template/concept/concept_generator.rb +145 -0
  9. data/lib/generators/tsykvas_rails_template/concept/templates/component/edit.html.slim.tt +5 -0
  10. data/lib/generators/tsykvas_rails_template/concept/templates/component/edit.rb.tt +11 -0
  11. data/lib/generators/tsykvas_rails_template/concept/templates/component/index.html.slim.tt +5 -0
  12. data/lib/generators/tsykvas_rails_template/concept/templates/component/index.rb.tt +11 -0
  13. data/lib/generators/tsykvas_rails_template/concept/templates/component/new.html.slim.tt +5 -0
  14. data/lib/generators/tsykvas_rails_template/concept/templates/component/new.rb.tt +11 -0
  15. data/lib/generators/tsykvas_rails_template/concept/templates/component/show.html.slim.tt +4 -0
  16. data/lib/generators/tsykvas_rails_template/concept/templates/component/show.rb.tt +11 -0
  17. data/lib/generators/tsykvas_rails_template/concept/templates/controller.rb.tt +45 -0
  18. data/lib/generators/tsykvas_rails_template/concept/templates/operation/create.rb.tt +31 -0
  19. data/lib/generators/tsykvas_rails_template/concept/templates/operation/destroy.rb.tt +13 -0
  20. data/lib/generators/tsykvas_rails_template/concept/templates/operation/edit.rb.tt +10 -0
  21. data/lib/generators/tsykvas_rails_template/concept/templates/operation/index.rb.tt +9 -0
  22. data/lib/generators/tsykvas_rails_template/concept/templates/operation/new.rb.tt +10 -0
  23. data/lib/generators/tsykvas_rails_template/concept/templates/operation/show.rb.tt +10 -0
  24. data/lib/generators/tsykvas_rails_template/concept/templates/operation/update.rb.tt +31 -0
  25. data/lib/generators/tsykvas_rails_template/install/bootstrap_installer.rb +225 -0
  26. data/lib/generators/tsykvas_rails_template/install/install_generator.rb +298 -0
  27. data/lib/generators/tsykvas_rails_template/install/templates/.claude/agents/buddy.md +157 -0
  28. data/lib/generators/tsykvas_rails_template/install/templates/.claude/agents/code-reviewer.md +117 -0
  29. data/lib/generators/tsykvas_rails_template/install/templates/.claude/agents/security-reviewer.md +113 -0
  30. data/lib/generators/tsykvas_rails_template/install/templates/.claude/agents/tech-lead.md +150 -0
  31. data/lib/generators/tsykvas_rails_template/install/templates/.claude/commands/check.md +51 -0
  32. data/lib/generators/tsykvas_rails_template/install/templates/.claude/commands/code-review.md +60 -0
  33. data/lib/generators/tsykvas_rails_template/install/templates/.claude/commands/docs-create.md +102 -0
  34. data/lib/generators/tsykvas_rails_template/install/templates/.claude/commands/pr-review.md +81 -0
  35. data/lib/generators/tsykvas_rails_template/install/templates/.claude/commands/pushit.md +160 -0
  36. data/lib/generators/tsykvas_rails_template/install/templates/.claude/commands/refactor.md +132 -0
  37. data/lib/generators/tsykvas_rails_template/install/templates/.claude/commands/task-sum.md +47 -0
  38. data/lib/generators/tsykvas_rails_template/install/templates/.claude/commands/tests.md +67 -0
  39. data/lib/generators/tsykvas_rails_template/install/templates/.claude/commands/tsykvas-claude.md +262 -0
  40. data/lib/generators/tsykvas_rails_template/install/templates/.claude/commands/update-docs.md +78 -0
  41. data/lib/generators/tsykvas_rails_template/install/templates/.claude/commands/update-rules.md +102 -0
  42. data/lib/generators/tsykvas_rails_template/install/templates/.claude/commands/update-tests.md +135 -0
  43. data/lib/generators/tsykvas_rails_template/install/templates/.claude/docs/architecture.md +315 -0
  44. data/lib/generators/tsykvas_rails_template/install/templates/.claude/docs/authentication.md +96 -0
  45. data/lib/generators/tsykvas_rails_template/install/templates/.claude/docs/background-jobs.md +135 -0
  46. data/lib/generators/tsykvas_rails_template/install/templates/.claude/docs/code-style.md +101 -0
  47. data/lib/generators/tsykvas_rails_template/install/templates/.claude/docs/commands.md +34 -0
  48. data/lib/generators/tsykvas_rails_template/install/templates/.claude/docs/companions.md +128 -0
  49. data/lib/generators/tsykvas_rails_template/install/templates/.claude/docs/concepts-refactoring.md +194 -0
  50. data/lib/generators/tsykvas_rails_template/install/templates/.claude/docs/database.md +135 -0
  51. data/lib/generators/tsykvas_rails_template/install/templates/.claude/docs/deployment.md +138 -0
  52. data/lib/generators/tsykvas_rails_template/install/templates/.claude/docs/design-system.md +322 -0
  53. data/lib/generators/tsykvas_rails_template/install/templates/.claude/docs/documentation.md +89 -0
  54. data/lib/generators/tsykvas_rails_template/install/templates/.claude/docs/forms.md +174 -0
  55. data/lib/generators/tsykvas_rails_template/install/templates/.claude/docs/i18n.md +165 -0
  56. data/lib/generators/tsykvas_rails_template/install/templates/.claude/docs/routing-and-namespaces.md +114 -0
  57. data/lib/generators/tsykvas_rails_template/install/templates/.claude/docs/security.md +122 -0
  58. data/lib/generators/tsykvas_rails_template/install/templates/.claude/docs/stimulus-controllers.md +166 -0
  59. data/lib/generators/tsykvas_rails_template/install/templates/.claude/docs/testing-examples.md +180 -0
  60. data/lib/generators/tsykvas_rails_template/install/templates/.claude/docs/testing.md +117 -0
  61. data/lib/generators/tsykvas_rails_template/install/templates/.claude/docs/tsykvas_rails_template.md +280 -0
  62. data/lib/generators/tsykvas_rails_template/install/templates/.claude/docs/ui-components.md +196 -0
  63. data/lib/generators/tsykvas_rails_template/install/templates/CLAUDE.md.tt +81 -0
  64. data/lib/generators/tsykvas_rails_template/install/templates/app/concepts/base/component/base.rb +6 -0
  65. data/lib/generators/tsykvas_rails_template/install/templates/app/concepts/base/operation/base.rb +124 -0
  66. data/lib/generators/tsykvas_rails_template/install/templates/app/concepts/base/operation/result.rb +56 -0
  67. data/lib/generators/tsykvas_rails_template/install/templates/app/concepts/home/component/index.html.slim +49 -0
  68. data/lib/generators/tsykvas_rails_template/install/templates/app/concepts/home/component/index.rb +11 -0
  69. data/lib/generators/tsykvas_rails_template/install/templates/app/concepts/home/operation/index.rb +17 -0
  70. data/lib/generators/tsykvas_rails_template/install/templates/app/controllers/concerns/operations_methods.rb +148 -0
  71. data/lib/generators/tsykvas_rails_template/install/templates/app/controllers/home_controller.rb +10 -0
  72. data/lib/generators/tsykvas_rails_template/install/templates/app/policies/application_policy.rb +33 -0
  73. data/lib/generators/tsykvas_rails_template/install/templates/app/policies/home_policy.rb +8 -0
  74. data/lib/tasks/tsykvas.rake +11 -0
  75. data/lib/tsykvas_rails_template/probe.rb +236 -0
  76. data/lib/tsykvas_rails_template/railtie.rb +13 -0
  77. data/lib/tsykvas_rails_template/version.rb +5 -0
  78. data/lib/tsykvas_rails_template.rb +18 -0
  79. 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,5 @@
1
+ h1= I18n.t("<%= i18n_key %>.edit.title")
2
+
3
+ = form_with model: @<%= singular_var %> do |f|
4
+ / TODO: form fields for <%= singular_var %>.
5
+ = f.submit
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ class <%= class_name %>::Component::Edit < ::Base::Component::Base
4
+ def initialize(<%= singular_var %>:)
5
+ @<%= singular_var %> = <%= singular_var %>
6
+ end
7
+
8
+ private
9
+
10
+ attr_reader :<%= singular_var %>
11
+ end
@@ -0,0 +1,5 @@
1
+ h1= I18n.t("<%= i18n_key %>.index.title")
2
+
3
+ ul
4
+ - @<%= plural_var %>.each do |<%= singular_var %>|
5
+ li= <%= singular_var %>.id
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ class <%= class_name %>::Component::Index < ::Base::Component::Base
4
+ def initialize(<%= plural_var %>: [])
5
+ @<%= plural_var %> = <%= plural_var %>
6
+ end
7
+
8
+ private
9
+
10
+ attr_reader :<%= plural_var %>
11
+ end
@@ -0,0 +1,5 @@
1
+ h1= I18n.t("<%= i18n_key %>.new.title")
2
+
3
+ = form_with model: @<%= singular_var %> do |f|
4
+ / TODO: form fields for <%= singular_var %>.
5
+ = f.submit
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ class <%= class_name %>::Component::New < ::Base::Component::Base
4
+ def initialize(<%= singular_var %>:)
5
+ @<%= singular_var %> = <%= singular_var %>
6
+ end
7
+
8
+ private
9
+
10
+ attr_reader :<%= singular_var %>
11
+ end
@@ -0,0 +1,4 @@
1
+ h1= I18n.t("<%= i18n_key %>.show.title")
2
+
3
+ / TODO: render @<%= singular_var %> attributes here.
4
+ p ID: #{@<%= singular_var %>.id}
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ class <%= class_name %>::Component::Show < ::Base::Component::Base
4
+ def initialize(<%= singular_var %>:)
5
+ @<%= singular_var %> = <%= singular_var %>
6
+ end
7
+
8
+ private
9
+
10
+ attr_reader :<%= singular_var %>
11
+ 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