Class: Ruact::Generators::ScaffoldGenerator
- Inherits:
-
Rails::Generators::NamedBase
- Object
- Rails::Generators::NamedBase
- Ruact::Generators::ScaffoldGenerator
- Includes:
- FormHelpers, ShadcnPreflight
- Defined in:
- lib/generators/ruact/scaffold/scaffold_generator.rb,
lib/generators/ruact/scaffold/scaffold_attribute.rb,
lib/generators/ruact/scaffold/scaffold_form_helpers.rb,
lib/generators/ruact/scaffold/scaffold_shadcn_preflight.rb
Overview
Generates a complete CRUD skeleton on the v2 route-driven contract:
rails generate ruact:scaffold Post title:string body:text published:boolean
Story 14.3 (FR102) — the scaffold is now self-contained with Rails parity:
it first DELEGATES to Rails' own public resource generator (model +
migration + the resources route + host-framework test stubs, honoring
config.generators test_framework), then OVERLAYS the ruact layer on top.
No manual rails generate model … step is required anymore.
Produces (Story 10.1 — the skeleton; Stories 10.2/10.3/10.4 upgrade the components to shadcn DataTable/Form/AlertDialog):
0. app/models/post.rb + db/migrate/*_create_posts.rb + host-framework
model/controller test stubs — generated by the delegated Rails
`resource` generator (14.3), which also draws `resources :posts`.
1. app/controllers/posts_controller.rb — seven RESTful actions, the v2
shape (`include Ruact::Server`, implicit `default_render` on GET,
Bucket-2 JSON on non-GET, server-driven `redirect_to`, FR98
`ruact_errors`, raw `:id` finders with commented FR96 opt-in). The
overlay `force`-overwrites `resource`'s bare controller (14.3 D2).
2. config/routes.rb — `resources :posts` injected.
3. app/javascript/components/PostList.tsx, PostForm.tsx,
PostDeleteDialog.tsx — plain working React components, typed against
the server boundary (FR99) and carrying an opt-in `__ruactContract`
(FR100). `--javascript` emits untyped `.jsx` instead.
4. app/views/posts/{index,show,new,edit}.html.erb — render the components
with props from controller-set ivars (no `as_json` in the view).
5. spec/requests/posts_spec.rb — a light controller smoke spec
(RSpec-only; emitted only when the host test framework is RSpec — 14.3
D4 — `force`-overwriting `resource`'s same-path RSpec request stub).
Prerequisite: rails generate ruact:install must have run first (it creates
app/javascript/.ruact/ and primes the route-driven codegen that emits the
createPost/updatePost/destroyPost accessors the components import).
rubocop:disable Metrics/ClassLength
Thor requires command methods (the public tasks) and class_option
declarations to live on the generator class itself; every non-command
helper is already extracted into a module (FormHelpers,
ShadcnPreflight) or class (ScaffoldAttribute). The remaining surface
is irreducible Thor command/option declarations + their view-model
accessors, so the class sits a little over the default length budget.
Defined Under Namespace
Modules: FormHelpers, ShadcnPreflight Classes: ScaffoldAttribute
Constant Summary collapse
- TYPE_MAP =
Supported attribute types → their TS wire type (FR99 wire-union grain: string | number | boolean | null) + the plain form control kind. The rich shadcn control mapping lands in 10.2/10.3; this stays minimal.
{ "string" => { ts: "string", control: :text }, "text" => { ts: "string", control: :textarea }, "integer" => { ts: "number", control: :number }, "float" => { ts: "number", control: :number }, "decimal" => { ts: "number", control: :number }, "boolean" => { ts: "boolean", control: :checkbox }, "date" => { ts: "string", control: :date }, "datetime" => { ts: "string", control: :datetime }, "references" => { ts: "number", control: :number } }.freeze
- SUPPORTED_TYPES =
TYPE_MAP.keys.freeze
- SEARCHABLE_COLUMN_TYPES =
Column types the
searchquery's case-insensitive LIKE scope spans — matching numeric/date/boolean columns by substring is meaningless. %w[string text].freeze
- REFERENCE_OPTIONS_LIMIT =
Story 10.3 (AC6) — parent-set size at/under which a
referencesfield renders an eager shadcn<Select>of controller-provided options. Above it, a server-search combobox is the right control; the generated Form leaves a documented opt-in trail (the parent-options query it would query is out of this scaffold's scope — the parent model isn't scaffolded here). Adjustable: re-run with a different value and re-generate, or edit the emitted controller's.limit(...). 100- DOCS_POINTER =
Documentation anchor referenced by the unknown-type error message (AC4).
"https://github.com/luizcg/ruact/blob/main/website/docs/api/scaffold.md#attribute-types"
Constants included from ShadcnPreflight
ShadcnPreflight::SHADCN_DOCS_POINTER
Constants included from FormHelpers
FormHelpers::INPUT_SHADCN_CONTROLS
Instance Method Summary collapse
-
#add_query_route ⇒ Object
AC5 — mount the resource query so its
searchmethod becomes the named GET route the codegen exports assearch(consumed byuseQuery). -
#add_resource_route ⇒ Object
AC5 — idempotent on re-run: Rails owns the insertion, but its
routeaction is not duplicate-aware across runs, so guard on the drawn line first. -
#check_shadcn_setup ⇒ Object
Story 10.5 (AC1, AC2, AC4) — the shadcn/ui dependency PRE-FLIGHT: detect the host's shadcn state (complete / missing / partial) and either proceed (complete), or print copy-pasteable
npx shadcnguidance and ABORT before any write (missing / partial), unless--skip-shadcn-checkbypasses the abort. -
#create_components ⇒ Object
Story 14.4 (FR103) / Story 14.5 (FR103b) — the component templates are the ONLY thing the
--shadcnopt-in flips: * default (no flag) → design-system-AGNOSTIC components (plain native HTML, Rails-default CSS, no shadcn/ui, no Tailwind) fromcomponents/agnostic/(Story 14.4). -
#create_controller ⇒ Object
Story 14.3 (AC4, D2) —
force: trueso ruact's v2 controller silently overwrites the empty 7-action controllerresourceemits (no Thor "conflict?" prompt, no leftover bare controller). -
#create_queries ⇒ Object
AC5 — the client-driven read path.
-
#create_smoke_spec ⇒ Object
Story 14.3 (AC2, AC4, D4) — ruact's v2-contract request spec is authoritative, but it is RSpec-only (
require "rails_helper",type: :request). - #create_views ⇒ Object
-
#generate_rails_resource ⇒ Object
Story 14.3 (FR102, AC1) — delegate model + migration + the
resourcesroute + host-framework test stubs to Rails' own publicresourcegenerator, THEN let the overlay tasks below win where they intersect (the controller force-overwrites resource's bare one; the route guard no-ops on the already-drawn line).
Methods included from ShadcnPreflight
#compute_shadcn_setup_state, #host_package_json, #installed_shadcn_major, #missing_shadcn_components, #missing_shadcn_message, #partial_shadcn_message, #required_shadcn_components, #run_shadcn_preflight!, #safe_parse_json, #shadcn_add_command, #shadcn_banner_add_command, #shadcn_component_file, #shadcn_config_path, #shadcn_missing?, #shadcn_setup_state, #shadcn_ui_dir, #shadcn_version_string, #skip_shadcn_check?, #warn_incompatible_shadcn_version
Methods included from FormHelpers
#form_contract_props, #form_props_params, #form_props_type, #form_uses_input?, #form_uses_select?, #form_uses_switch?, #form_uses_textarea?, #html_input_type, #native_input_type, #reference_attributes, #reference_options_expr, #reference_options_limit
Instance Method Details
#add_query_route ⇒ Object
AC5 — mount the resource query so its search method becomes the named
GET route the codegen exports as search (consumed by useQuery).
Idempotent on re-run: guard on the drawn ruact_queries <Plural>Query
line first (sibling of #add_resource_route's resources :posts guard).
212 213 214 215 216 217 218 219 220 |
# File 'lib/generators/ruact/scaffold/scaffold_generator.rb', line 212 def add_query_route routes_file = Pathname(destination_root).join("config/routes.rb") if routes_file.exist? && routes_file.read.match?(/^\s*ruact_queries\s+#{Regexp.escape(query_class_name)}\b/) say_status "skip", "ruact_queries #{query_class_name} already routed", :yellow return end route "ruact_queries #{query_class_name}" end |
#add_resource_route ⇒ Object
AC5 — idempotent on re-run: Rails owns the insertion, but its route
action is not duplicate-aware across runs, so guard on the drawn line
first. (A commented collection { post :publish } FR96 anchor is NOT
injected here — Rails' route action only takes a single statement; the
publish demo lives commented in the controller, where it reads cleanly.)
183 184 185 186 187 188 189 190 191 |
# File 'lib/generators/ruact/scaffold/scaffold_generator.rb', line 183 def add_resource_route routes_file = Pathname(destination_root).join("config/routes.rb") if routes_file.exist? && routes_file.read.match?(/^\s*resources +:#{Regexp.escape(plural_name)}\b/) say_status "skip", "resources :#{plural_name} already routed", :yellow return end route "resources :#{plural_name}" end |
#check_shadcn_setup ⇒ Object
Story 10.5 (AC1, AC2, AC4) — the shadcn/ui dependency PRE-FLIGHT: detect
the host's shadcn state (complete / missing / partial) and either proceed
(complete), or print copy-pasteable npx shadcn guidance and ABORT
before any write (missing / partial), unless --skip-shadcn-check
bypasses the abort. The generator NEVER auto-runs npx/npm (AC4 — no
surprising network/install behavior, critical for CI). Aborting via
raise Thor::Error mirrors assert_supported_attribute_types!.
Story 14.5 (FR103b, D1) — RE-PROMOTED to a Thor command (Story 14.4 had
de-registered it via remove_command so the default path never aborted).
It is now --shadcn-GATED: the return unless shadcn? guard makes it a
no-op on the default (agnostic) path — a fresh app with no shadcn still
scaffolds without a pre-flight or abort — while under --shadcn it runs
the byte-preserved ShadcnPreflight detection / guidance / abort (zero
partial state, raise Thor::Error on missing/partial unless
--skip-shadcn-check). Declared FIRST among the command tasks (before
#generate_rails_resource) so the --shadcn abort precedes every write.
143 144 145 146 147 |
# File 'lib/generators/ruact/scaffold/scaffold_generator.rb', line 143 def check_shadcn_setup return unless shadcn? run_shadcn_preflight! # {ShadcnPreflight} — detection / guidance / abort end |
#create_components ⇒ Object
Story 14.4 (FR103) / Story 14.5 (FR103b) — the component templates are the
ONLY thing the --shadcn opt-in flips:
* default (no flag) → design-system-AGNOSTIC components (plain native
HTML, Rails-default CSS, no shadcn/ui, no Tailwind) from
`components/agnostic/` (Story 14.4).
* `--shadcn` → the byte-preserved Epic 10 shadcn DataTable / Form /
AlertDialog templates from `components/` (Story 14.5 — restored from
behind the flag, NOT rewritten).
component_ext (.tsx / .jsx) is selected independently so
--shadcn --javascript composes (untyped .jsx shadcn output). Everything
else (controller, route, query, views, smoke spec) is identical on both
paths.
240 241 242 243 244 245 246 247 248 |
# File 'lib/generators/ruact/scaffold/scaffold_generator.rb', line 240 def create_components dir = shadcn? ? "components" : "components/agnostic" template "#{dir}/List.tsx.tt", File.join("app/javascript/components", "#{class_name}List.#{component_ext}") template "#{dir}/Form.tsx.tt", File.join("app/javascript/components", "#{class_name}Form.#{component_ext}") template "#{dir}/DeleteDialog.tsx.tt", File.join("app/javascript/components", "#{class_name}DeleteDialog.#{component_ext}") end |
#create_controller ⇒ Object
Story 14.3 (AC4, D2) — force: true so ruact's v2 controller silently
overwrites the empty 7-action controller resource emits (no Thor
"conflict?" prompt, no leftover bare controller). The overlay is the
real controller; resource's controller-test stub is harmless and stands
as the Rails-parity controller test.
173 174 175 176 |
# File 'lib/generators/ruact/scaffold/scaffold_generator.rb', line 173 def create_controller template "controller.rb.tt", File.join("app/controllers", "#{plural_file_name}_controller.rb"), force: true end |
#create_queries ⇒ Object
AC5 — the client-driven read path. Emits the resource query
(<Plural>Query < ApplicationQuery with a search(q:) method) in BOTH
.tsx and .jsx modes (the query is server-side Ruby; the language flag
only governs the React component). The ApplicationQuery base is created
idempotently — ruact:install does NOT ship it, and a second scaffold in
the same app must NOT clobber a customized base.
199 200 201 202 203 204 205 206 |
# File 'lib/generators/ruact/scaffold/scaffold_generator.rb', line 199 def create_queries template "queries/query.rb.tt", File.join("app/queries", "#{plural_file_name}_query.rb") application_query = File.join("app/queries", "application_query.rb") return if Pathname(destination_root).join(application_query).exist? template "queries/application_query.rb.tt", application_query end |
#create_smoke_spec ⇒ Object
Story 14.3 (AC2, AC4, D4) — ruact's v2-contract request spec is
authoritative, but it is RSpec-only (require "rails_helper",
type: :request). Two reconciliations:
1. Framework gate — emit ONLY when the host test framework is RSpec, so
a Minitest app never gets a broken `rails_helper`-requiring file
(the delegated Rails Minitest model/controller tests stand alone).
2. RSpec-path collision — with `rspec-rails`, `resource` emits a request
spec at this SAME path; `force: true` makes ruact's v2 spec win, so
there is exactly one request spec and it is ruact's.
259 260 261 262 263 264 |
# File 'lib/generators/ruact/scaffold/scaffold_generator.rb', line 259 def create_smoke_spec return unless rspec_smoke_spec? template "request_spec.rb.tt", File.join("spec/requests", "#{plural_file_name}_spec.rb"), force: true end |
#create_views ⇒ Object
222 223 224 225 226 |
# File 'lib/generators/ruact/scaffold/scaffold_generator.rb', line 222 def create_views %w[index show new edit].each do |view| template "views/#{view}.html.erb.tt", File.join("app/views", plural_file_name, "#{view}.html.erb") end end |
#generate_rails_resource ⇒ Object
Story 14.3 (FR102, AC1) — delegate model + migration + the resources
route + host-framework test stubs to Rails' own public resource
generator, THEN let the overlay tasks below win where they intersect
(the controller force-overwrites resource's bare one; the route guard
no-ops on the already-drawn line). Declared right AFTER the --shadcn-gated
pre-flight #check_shadcn_setup (Story 14.5) so a --shadcn missing-setup
abort precedes this delegation (zero partial state); on the default path
the pre-flight no-ops and this runs first among the writing tasks. A bad
input still fails loud at parse_attributes!, before any task runs (zero
partial state). The real
sub-generator call lives behind the stubbable invoke_rails_resource
seam (mirroring Story 14.1's run_npm_install) — the unit specs stub it
(a bare mktmpdir is not a booted Rails app, so a real invoke "resource" cannot run there); the live end-to-end delegation is proven
by Story 14.6's clean-room Docker E2E.
164 165 166 |
# File 'lib/generators/ruact/scaffold/scaffold_generator.rb', line 164 def generate_rails_resource invoke_rails_resource(name, *raw_attribute_args) end |