Class: Ruact::Generators::ScaffoldGenerator

Inherits:
Rails::Generators::NamedBase
  • Object
show all
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 search query'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 references field 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

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_routeObject

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_routeObject

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_setupObject

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_componentsObject

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_controllerObject

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_queriesObject

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_specObject

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_viewsObject



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_resourceObject

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