Skip to content

Commit a83f16d

Browse files
authored
fix(cli): emit validatesPresenceOf and type validations from model attrs (#2219) (#2224)
Typed attrs passed to `wheels generate model` (e.g. `name:string email:email`) were parsed into the migration but dropped from the generated model's config(), producing an empty scaffold. Populate context.validations in CodeGen.generateModel from the properties array, add a {{validations}} placeholder to ModelContent.txt, and update the Templates.cfc substitution block to consume the pre-built string (previously it unconditionally overwrote the generic-loop output with "" because its only input was a never-set context.attributes). Emits combined validatesPresenceOf("a,b,c") for all attrs plus per-property validatesFormatOf for email and url types. Scaffold and api-resource inherit the fix since both route through CodeGen.generateModel. Strengthens tools/ci/smoke-test-module.sh to assert the new behavior and adds CodeGenSpec coverage for combined presence, email format, URL format, and the empty-properties case.
1 parent 4e8b1bb commit a83f16d

7 files changed

Lines changed: 89 additions & 10 deletions

File tree

app/snippets/ModelContent.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
{{belongsToRelationships}}
66
{{hasManyRelationships}}
77
{{hasOneRelationships}}
8+
{{validations}}
89
}
910

1011
}

cli/lucli/services/CodeGen.cfc

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@ component {
4949
belongsTo: arguments.belongsTo,
5050
hasMany: arguments.hasMany,
5151
hasOne: arguments.hasOne,
52+
validations: buildModelValidations(arguments.properties),
5253
timestamp: dateTimeFormat(now(), "yyyy-mm-dd HH:nn:ss")
5354
};
5455

@@ -61,6 +62,32 @@ component {
6162
return result;
6263
}
6364

65+
/**
66+
* Build validation code lines for a model's config() from typed properties.
67+
* Emits a single combined validatesPresenceOf("a,b,c") for all properties,
68+
* plus per-property validatesFormatOf for email and URL types.
69+
*/
70+
private string function buildModelValidations(required array properties) {
71+
if (!arrayLen(arguments.properties)) return "";
72+
73+
var presenceProps = [];
74+
var formatLines = [];
75+
76+
for (var prop in arguments.properties) {
77+
arrayAppend(presenceProps, prop.name);
78+
var propType = structKeyExists(prop, "type") ? lCase(prop.type) : "string";
79+
if (propType == "email") {
80+
arrayAppend(formatLines, " validatesFormatOf(property=""#prop.name#"", type=""email"");");
81+
} else if (propType == "url") {
82+
arrayAppend(formatLines, " validatesFormatOf(property=""#prop.name#"", type=""URL"");");
83+
}
84+
}
85+
86+
var lines = [" validatesPresenceOf(""#arrayToList(presenceProps)#"");"];
87+
lines.append(formatLines, true);
88+
return arrayToList(lines, chr(10));
89+
}
90+
6491
/**
6592
* Generate a controller file
6693
*/

cli/lucli/services/Templates.cfc

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -90,14 +90,18 @@ component {
9090
// Process relationship placeholders
9191
processed = processRelationships(processed, arguments.context);
9292

93-
// Process attributes/validations
94-
if (structKeyExists(arguments.context, "attributes") && len(arguments.context.attributes)) {
93+
// Process {{validations}} placeholder. Prefer the pre-built `validations`
94+
// code string from context (populated by CodeGen.generateModel); fall back
95+
// to deriving from the legacy `attributes` string shape for callers that
96+
// still use it.
97+
var validationCode = "";
98+
if (structKeyExists(arguments.context, "validations") && isSimpleValue(arguments.context.validations)) {
99+
validationCode = arguments.context.validations;
100+
} else if (structKeyExists(arguments.context, "attributes") && len(arguments.context.attributes)) {
95101
var attributesStruct = parseAttributes(arguments.context.attributes);
96-
var validationCode = generateValidationCode(attributesStruct);
97-
processed = reReplace(processed, "\{\{validations\}\}", validationCode, "all");
98-
} else {
99-
processed = reReplace(processed, "\{\{validations\}\}", "", "all");
102+
validationCode = generateValidationCode(attributesStruct);
100103
}
104+
processed = reReplace(processed, "\{\{validations\}\}", validationCode, "all");
101105

102106
// Process actions for controllers
103107
if (structKeyExists(arguments.context, "actions") && isArray(arguments.context.actions)) {

cli/lucli/templates/app/app/snippets/ModelContent.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
{{belongsToRelationships}}
66
{{hasManyRelationships}}
77
{{hasOneRelationships}}
8+
{{validations}}
89
}
910

1011
}

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

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,50 @@ component extends="wheels.wheelstest.system.BaseSpec" {
5353
expect(content).toInclude("config()");
5454
});
5555

56+
it("emits validatesPresenceOf combining all properties (##2219)", () => {
57+
codegen.generateModel(
58+
name = "Foo",
59+
properties = [
60+
{name: "bar", type: "string"},
61+
{name: "baz", type: "integer"}
62+
],
63+
force = true
64+
);
65+
var content = fileRead(tempRoot & "/app/models/Foo.cfc");
66+
expect(content).toInclude('validatesPresenceOf("bar,baz")');
67+
});
68+
69+
it("emits validatesFormatOf for email-typed properties (##2219)", () => {
70+
codegen.generateModel(
71+
name = "Subscriber",
72+
properties = [
73+
{name: "name", type: "string"},
74+
{name: "email", type: "email"}
75+
],
76+
force = true
77+
);
78+
var content = fileRead(tempRoot & "/app/models/Subscriber.cfc");
79+
expect(content).toInclude('validatesPresenceOf("name,email")');
80+
expect(content).toInclude('validatesFormatOf(property="email", type="email")');
81+
});
82+
83+
it("emits validatesFormatOf for url-typed properties (##2219)", () => {
84+
codegen.generateModel(
85+
name = "Link",
86+
properties = [{name: "website", type: "url"}],
87+
force = true
88+
);
89+
var content = fileRead(tempRoot & "/app/models/Link.cfc");
90+
expect(content).toInclude('validatesFormatOf(property="website", type="URL")');
91+
});
92+
93+
it("skips validations when no properties given (##2219)", () => {
94+
codegen.generateModel(name = "Empty", properties = [], force = true);
95+
var content = fileRead(tempRoot & "/app/models/Empty.cfc");
96+
expect(content).notToInclude("validatesPresenceOf");
97+
expect(content).notToInclude("validatesFormatOf");
98+
});
99+
56100
});
57101

58102
describe("generateController()", () => {

cli/src/templates/ModelContent.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
{{belongsToRelationships}}
66
{{hasManyRelationships}}
77
{{hasOneRelationships}}
8+
{{validations}}
89
}
910

1011
}

tools/ci/smoke-test-module.sh

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -145,13 +145,14 @@ echo ""
145145
echo "=== Phase 4: Exercise every template-driven generator ==="
146146

147147
echo ""
148-
echo "--- generate model User name:string email:string ---"
149-
lucli wheels generate model User name:string email:string 2>&1 | sed 's/^/ /' || true
148+
echo "--- generate model User name:string email:email ---"
149+
lucli wheels generate model User name:string email:email 2>&1 | sed 's/^/ /' || true
150150
assert_file_nonempty "app/models/User.cfc" "model User.cfc generated"
151-
# Current ModelContent template renders component shell + config() scaffold but
152-
# does not emit validations from attrs. Assert structure, not content we don't emit.
153151
assert_grep 'extends="Model"' "app/models/User.cfc" "model extends Model"
154152
assert_grep "function config" "app/models/User.cfc" "model has config()"
153+
# Typed attrs must round-trip into validations in config(). See #2219.
154+
assert_grep 'validatesPresenceOf\("name,email"\)' "app/models/User.cfc" "model emits validatesPresenceOf for attrs"
155+
assert_grep 'validatesFormatOf\(property="email", type="email"\)' "app/models/User.cfc" "model emits validatesFormatOf for email-typed attr"
155156

156157
echo ""
157158
echo "--- generate controller Users index show ---"

0 commit comments

Comments
 (0)