Skip to content

Commit 0be23b4

Browse files
authored
[TypeSpecValidation] Enforce ".Management" suffix (#29663)
- Fixes #29654
1 parent f32a626 commit 0be23b4

File tree

2 files changed

+121
-10
lines changed

2 files changed

+121
-10
lines changed

eng/tools/typespec-validation/src/rules/folder-structure.ts

Lines changed: 31 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import path from "path";
2+
import { parse as yamlParse } from "yaml";
23
import { Rule } from "../rule.js";
34
import { RuleResult } from "../rule-result.js";
45
import { TsvHost } from "../tsv-host.js";
@@ -10,8 +11,8 @@ export class FolderStructureRule implements Rule {
1011
let success = true;
1112
let stdOutput = "";
1213
let errorOutput = "";
13-
let gitRoot = host.normalizePath(await host.gitOperation(folder).revparse("--show-toplevel"));
14-
let relativePath = path.relative(gitRoot, folder).split(path.sep).join("/");
14+
const gitRoot = host.normalizePath(await host.gitOperation(folder).revparse("--show-toplevel"));
15+
const relativePath = path.relative(gitRoot, folder).split(path.sep).join("/");
1516

1617
stdOutput += `folder: ${folder}\n`;
1718
if (!(await host.checkFileExists(folder))) {
@@ -32,13 +33,13 @@ export class FolderStructureRule implements Rule {
3233
});
3334

3435
// Verify top level folder is lower case and remove empty entries when splitting by slash
35-
let folderStruct = relativePath.split("/").filter(Boolean);
36+
const folderStruct = relativePath.split("/").filter(Boolean);
3637
if (folderStruct[1].match(/[A-Z]/g)) {
3738
success = false;
3839
errorOutput += `Invalid folder name. Folders under specification/ must be lower case.\n`;
3940
}
4041

41-
let packageFolder = folderStruct[folderStruct.length - 1];
42+
const packageFolder = folderStruct[folderStruct.length - 1];
4243

4344
// Verify package folder is at most 3 levels deep
4445
if (folderStruct.length > 4) {
@@ -61,8 +62,9 @@ export class FolderStructureRule implements Rule {
6162
}
6263

6364
// Verify tspconfig, main.tsp, examples/
64-
let mainExists = await host.checkFileExists(path.join(folder, "main.tsp"));
65-
let clientExists = await host.checkFileExists(path.join(folder, "client.tsp"));
65+
const mainExists = await host.checkFileExists(path.join(folder, "main.tsp"));
66+
const clientExists = await host.checkFileExists(path.join(folder, "client.tsp"));
67+
const tspConfigExists = await host.checkFileExists(path.join(folder, "tspconfig.yaml"));
6668

6769
if (!mainExists && !clientExists) {
6870
errorOutput += `Invalid folder structure: Spec folder must contain main.tsp or client.tsp.`;
@@ -74,14 +76,33 @@ export class FolderStructureRule implements Rule {
7476
success = false;
7577
}
7678

77-
if (
78-
!packageFolder.includes("Shared") &&
79-
!(await host.checkFileExists(path.join(folder, "tspconfig.yaml")))
80-
) {
79+
if (!packageFolder.includes("Shared") && !tspConfigExists) {
8180
errorOutput += `Invalid folder structure: Spec folder must contain tspconfig.yaml.`;
8281
success = false;
8382
}
8483

84+
if (tspConfigExists) {
85+
const configText = await host.readTspConfig(folder);
86+
const config = yamlParse(configText);
87+
const rpFolder =
88+
config?.options?.["@azure-tools/typespec-autorest"]?.["azure-resource-provider-folder"];
89+
stdOutput += `azure-resource-provider-folder: ${JSON.stringify(rpFolder)}\n`;
90+
91+
if (
92+
rpFolder?.trim()?.endsWith("resource-manager") &&
93+
!packageFolder.endsWith(".Management")
94+
) {
95+
errorOutput += `Invalid folder structure: TypeSpec for resource-manager specs must be in a folder ending with '.Management'`;
96+
success = false;
97+
} else if (
98+
!rpFolder?.trim()?.endsWith("resource-manager") &&
99+
packageFolder.endsWith(".Management")
100+
) {
101+
errorOutput += `Invalid folder structure: TypeSpec for data-plane specs or shared code must be in a folder NOT ending with '.Management'`;
102+
success = false;
103+
}
104+
}
105+
85106
return {
86107
success: success,
87108
stdOutput: stdOutput,

eng/tools/typespec-validation/test/folder-structure.test.ts

Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -205,4 +205,94 @@ describe("folder-structure", function () {
205205
assert(result.errorOutput);
206206
assert(result.errorOutput.includes("must contain"));
207207
});
208+
209+
it("should succeed with resource-manager/Management", async function() {
210+
let host = new TsvTestHost();
211+
host.globby = async () => {
212+
return ["/foo/Foo.Management/tspconfig.yaml"];
213+
};
214+
host.normalizePath = () => {
215+
return "/gitroot";
216+
};
217+
host.readTspConfig = async (_folder: string) => `
218+
options:
219+
"@azure-tools/typespec-autorest":
220+
azure-resource-provider-folder: "resource-manager"
221+
`;
222+
223+
const result = await new FolderStructureRule().execute(
224+
host,
225+
"/gitroot/specification/foo/Foo.Management",
226+
);
227+
228+
assert(result.success);
229+
});
230+
231+
it("should succeed with data-plane/NoManagement", async function() {
232+
let host = new TsvTestHost();
233+
host.globby = async () => {
234+
return ["/foo/Foo/tspconfig.yaml"];
235+
};
236+
host.normalizePath = () => {
237+
return "/gitroot";
238+
};
239+
host.readTspConfig = async (_folder: string) => `
240+
options:
241+
"@azure-tools/typespec-autorest":
242+
azure-resource-provider-folder: "data-plane"
243+
`;
244+
245+
const result = await new FolderStructureRule().execute(
246+
host,
247+
"/gitroot/specification/foo/Foo",
248+
);
249+
250+
assert(result.success);
251+
});
252+
253+
it("should fail with resource-manager/NoManagement", async function() {
254+
let host = new TsvTestHost();
255+
host.globby = async () => {
256+
return ["/foo/Foo/tspconfig.yaml"];
257+
};
258+
host.normalizePath = () => {
259+
return "/gitroot";
260+
};
261+
host.readTspConfig = async (_folder: string) => `
262+
options:
263+
"@azure-tools/typespec-autorest":
264+
azure-resource-provider-folder: "resource-manager"
265+
`;
266+
267+
const result = await new FolderStructureRule().execute(
268+
host,
269+
"/gitroot/specification/foo/Foo",
270+
);
271+
272+
assert(result.errorOutput);
273+
assert(result.errorOutput.includes(".Management"));
274+
});
275+
276+
it("should fail with data-plane/Management", async function() {
277+
let host = new TsvTestHost();
278+
host.globby = async () => {
279+
return ["/foo/Foo.Management/tspconfig.yaml"];
280+
};
281+
host.normalizePath = () => {
282+
return "/gitroot";
283+
};
284+
host.readTspConfig = async (_folder: string) => `
285+
options:
286+
"@azure-tools/typespec-autorest":
287+
azure-resource-provider-folder: "data-plane"
288+
`;
289+
290+
const result = await new FolderStructureRule().execute(
291+
host,
292+
"/gitroot/specification/foo/Foo.Management",
293+
);
294+
295+
assert(result.errorOutput);
296+
assert(result.errorOutput.includes(".Management"));
297+
});
208298
});

0 commit comments

Comments
 (0)