Skip to content

Commit cca91dc

Browse files
wheels-bot[bot]github-actions[bot]claudebpamiri
authored
fix(cli): split comma-joined action tokens in generate controller (#3131)
* fix(cli): split comma-joined action tokens in generate controller `wheels generate controller Name a,b` passed the comma-joined token through verbatim, emitting an invalid `function a,b()` method plus an `a,b.cfm` view file. Normalize the action list in CodeGen.generateController by splitting each positional token on commas, trimming, and de-duplicating, and return the normalized list so Module.cfc renders one view file per real action. The comma form now behaves identically to the documented space-separated form. Fixes #3112 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Signed-off-by: claude[bot] <41898282+claude[bot]@users.noreply.github.com> * docs(web/guides): note comma-separated action form in generate controller Signed-off-by: claude[bot] <41898282+claude[bot]@users.noreply.github.com> * fix(cli): keep generate controller no-actions path from writing an index view Review finding on #3131: the unconditional `actions = result.actions` in Module.cfc adopted CodeGen's defaulted ["index"] list when the user passed no actions, so `wheels generate controller Users` started writing app/views/users/index.cfm — contradicting the documented "passing no actions creates an empty controller with no view files" behavior retained in the same code-generation.mdx paragraph. Surface the caller-requested (post-normalization, pre-default) action list as result.actions instead: empty input stays empty, so the view loop writes nothing, while comma-joined tokens still expand to one view per real action. The controller body keeps its default index() stub as before. Covered by a new CodeGenSpec case asserting result.actions is empty (and the stub present) for the no-actions path. Signed-off-by: Peter Amiri <peter@alurium.com> --------- Signed-off-by: claude[bot] <41898282+claude[bot]@users.noreply.github.com> Signed-off-by: Peter Amiri <peter@alurium.com> Co-authored-by: claude[bot] <41898282+claude[bot]@users.noreply.github.com> Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com> Co-authored-by: Peter Amiri <peter@alurium.com>
1 parent 2257dd6 commit cca91dc

5 files changed

Lines changed: 96 additions & 2 deletions

File tree

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
- `wheels generate controller Name a,b` now splits the comma-joined action token into discrete actions (`a` and `b`) instead of silently emitting an invalid `function a,b()` method plus an `a,b.cfm` view file — the comma form now matches the documented space-separated form (#3112)

cli/lucli/Module.cfc

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3253,6 +3253,13 @@ component extends="modules.BaseModule" {
32533253
return "";
32543254
}
32553255

3256+
// Use the normalized action list from the generator (comma-joined tokens like
3257+
// "index,show" are split into discrete actions) so view files match the
3258+
// controller methods instead of being named "index,show.cfm" (#3112). When no
3259+
// actions were passed result.actions is empty — documented behavior is an
3260+
// empty controller with no view files, so the view loop below writes nothing.
3261+
actions = result.actions;
3262+
32563263
// Create view files for non-mutation actions
32573264
var viewDir = variables.projectRoot & "/app/views/#lCase(controllerName)#";
32583265
ensureDirectory(viewDir);

cli/lucli/services/CodeGen.cfc

Lines changed: 41 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -142,6 +142,19 @@ component {
142142

143143
var crudActions = ["index", "show", "new", "create", "edit", "update", "delete"];
144144

145+
// Normalize the action list: a comma-joined token like "index,show" is the
146+
// natural guess for anyone used to Wheels list args (validatesPresenceOf("a,b")),
147+
// but passed through verbatim it produced `function index,show()` — invalid CFML
148+
// that fails to compile — plus a view file named `index,show.cfm` (#3112). Split
149+
// each token on commas, trim, and de-duplicate so both forms behave identically.
150+
arguments.actions = normalizeActions(arguments.actions);
151+
152+
// Capture the caller-requested list BEFORE the defaulting below. result.actions
153+
// drives the caller's view loop, and the documented contract is "passing no
154+
// actions creates an empty controller with no view files" — the index/CRUD
155+
// defaults applied next shape the controller body only, never the view files.
156+
var requestedActions = arguments.actions;
157+
145158
// Default actions based on type
146159
if (arrayLen(arguments.actions) == 0) {
147160
arguments.actions = arguments.crud ? crudActions : ["index"];
@@ -171,11 +184,38 @@ component {
171184
template = hasCustomActions ? "ControllerContent.txt" : "CRUDContent.txt";
172185
}
173186

174-
return variables.templateService.generateFromTemplate(
187+
var result = variables.templateService.generateFromTemplate(
175188
template = template,
176189
destination = relativePath,
177190
context = context
178191
);
192+
193+
// Surface the normalized caller-requested action list so callers (e.g.
194+
// Module.cfc's view loop) render one view file per real action instead of one
195+
// named after the raw comma-joined token (#3112). Deliberately the pre-default
196+
// list: when no actions were passed this stays empty, so callers write no view
197+
// files even though the controller body gets a default index() stub.
198+
result.actions = requestedActions;
199+
return result;
200+
}
201+
202+
/**
203+
* Flatten a positional action list into discrete, trimmed, de-duplicated action
204+
* names. Splits comma-joined tokens ("index,show" -> ["index","show"]) so the comma
205+
* form matches the documented space-separated form, drops empties, and preserves
206+
* first-seen order. Comparison is case-insensitive but the original casing is kept (#3112).
207+
*/
208+
private array function normalizeActions(required array actions) {
209+
var normalized = [];
210+
for (var token in arguments.actions) {
211+
for (var part in listToArray(token, ",")) {
212+
var trimmed = trim(part);
213+
if (len(trimmed) && !arrayFindNoCase(normalized, trimmed)) {
214+
arrayAppend(normalized, trimmed);
215+
}
216+
}
217+
}
218+
return normalized;
179219
}
180220

181221
/**

cli/lucli/tests/specs/services/CodeGenSpec.cfc

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -219,6 +219,52 @@ component extends="wheels.wheelstest.system.BaseSpec" {
219219
expect(content).toInclude('extends="Controller"');
220220
});
221221

222+
it("splits a comma-joined action token into separate actions (##3112)", () => {
223+
var result = codegen.generateController(
224+
name = "Authors",
225+
actions = ["index,show"],
226+
force = true
227+
);
228+
var content = fileRead(tempRoot & "/app/controllers/Authors.cfc");
229+
// The bug: one element "index,show" emitted as `function index,show()`
230+
expect(content).notToInclude("index,show");
231+
expect(content).toInclude("function index()");
232+
expect(content).toInclude("function show()");
233+
});
234+
235+
it("returns the normalized action list so callers render correct views (##3112)", () => {
236+
var result = codegen.generateController(
237+
name = "Editors",
238+
actions = ["index, show ,create"],
239+
force = true
240+
);
241+
expect(result.actions).toBe(["index", "show", "create"]);
242+
});
243+
244+
it("de-duplicates and trims actions from comma tokens (##3112)", () => {
245+
var result = codegen.generateController(
246+
name = "Curators",
247+
actions = ["index", "show,index"],
248+
force = true
249+
);
250+
expect(result.actions).toBe(["index", "show"]);
251+
});
252+
253+
it("returns an empty action list when no actions are passed so callers write no views", () => {
254+
var result = codegen.generateController(
255+
name = "Stubs",
256+
actions = [],
257+
force = true
258+
);
259+
var content = fileRead(tempRoot & "/app/controllers/Stubs.cfc");
260+
// The controller body still gets the default index() stub...
261+
expect(content).toInclude("function index()");
262+
// ...but result.actions stays empty so the caller writes no view
263+
// files, preserving the documented "no actions => empty controller
264+
// with no view files" behavior (PR ##3131 review).
265+
expect(result.actions).toBeEmpty();
266+
});
267+
222268
});
223269

224270
describe("validateName()", () => {

web/sites/guides/src/content/docs/v4-0-0/command-line-tools/wheels-commands/code-generation.mdx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -106,7 +106,7 @@ wheels generate controller <Name> [action ...]
106106

107107
#### Description
108108

109-
Writes `app/controllers/<Name>.cfc` with a stub method per action name you pass. For each action that isn't `create`, `update`, `delete`, or `destroy`, the generator also writes `app/views/<name>/<action>.cfm`. Passing no actions creates an empty controller with no view files.
109+
Writes `app/controllers/<Name>.cfc` with a stub method per action name you pass. For each action that isn't `create`, `update`, `delete`, or `destroy`, the generator also writes `app/views/<name>/<action>.cfm`. Passing no actions creates an empty controller with no view files. Action names may be space-separated or comma-separated — `index,show` is equivalent to `index show`.
110110

111111
#### Example
112112

0 commit comments

Comments
 (0)