diff --git a/eng/tools/typespec-validation/src/rules/folder-structure.ts b/eng/tools/typespec-validation/src/rules/folder-structure.ts index f6feb3327407..9e010e12d664 100644 --- a/eng/tools/typespec-validation/src/rules/folder-structure.ts +++ b/eng/tools/typespec-validation/src/rules/folder-structure.ts @@ -1,4 +1,5 @@ import path from "path"; +import { parse as yamlParse } from "yaml"; import { Rule } from "../rule.js"; import { RuleResult } from "../rule-result.js"; import { TsvHost } from "../tsv-host.js"; @@ -10,8 +11,8 @@ export class FolderStructureRule implements Rule { let success = true; let stdOutput = ""; let errorOutput = ""; - let gitRoot = host.normalizePath(await host.gitOperation(folder).revparse("--show-toplevel")); - let relativePath = path.relative(gitRoot, folder).split(path.sep).join("/"); + const gitRoot = host.normalizePath(await host.gitOperation(folder).revparse("--show-toplevel")); + const relativePath = path.relative(gitRoot, folder).split(path.sep).join("/"); stdOutput += `folder: ${folder}\n`; if (!(await host.checkFileExists(folder))) { @@ -32,13 +33,13 @@ export class FolderStructureRule implements Rule { }); // Verify top level folder is lower case and remove empty entries when splitting by slash - let folderStruct = relativePath.split("/").filter(Boolean); + const folderStruct = relativePath.split("/").filter(Boolean); if (folderStruct[1].match(/[A-Z]/g)) { success = false; errorOutput += `Invalid folder name. Folders under specification/ must be lower case.\n`; } - let packageFolder = folderStruct[folderStruct.length - 1]; + const packageFolder = folderStruct[folderStruct.length - 1]; // Verify package folder is at most 3 levels deep if (folderStruct.length > 4) { @@ -61,8 +62,9 @@ export class FolderStructureRule implements Rule { } // Verify tspconfig, main.tsp, examples/ - let mainExists = await host.checkFileExists(path.join(folder, "main.tsp")); - let clientExists = await host.checkFileExists(path.join(folder, "client.tsp")); + const mainExists = await host.checkFileExists(path.join(folder, "main.tsp")); + const clientExists = await host.checkFileExists(path.join(folder, "client.tsp")); + const tspConfigExists = await host.checkFileExists(path.join(folder, "tspconfig.yaml")); if (!mainExists && !clientExists) { errorOutput += `Invalid folder structure: Spec folder must contain main.tsp or client.tsp.`; @@ -74,14 +76,33 @@ export class FolderStructureRule implements Rule { success = false; } - if ( - !packageFolder.includes("Shared") && - !(await host.checkFileExists(path.join(folder, "tspconfig.yaml"))) - ) { + if (!packageFolder.includes("Shared") && !tspConfigExists) { errorOutput += `Invalid folder structure: Spec folder must contain tspconfig.yaml.`; success = false; } + if (tspConfigExists) { + const configText = await host.readTspConfig(folder); + const config = yamlParse(configText); + const rpFolder = + config?.options?.["@azure-tools/typespec-autorest"]?.["azure-resource-provider-folder"]; + stdOutput += `azure-resource-provider-folder: ${JSON.stringify(rpFolder)}\n`; + + if ( + rpFolder?.trim()?.endsWith("resource-manager") && + !packageFolder.endsWith(".Management") + ) { + errorOutput += `Invalid folder structure: TypeSpec for resource-manager specs must be in a folder ending with '.Management'`; + success = false; + } else if ( + !rpFolder?.trim()?.endsWith("resource-manager") && + packageFolder.endsWith(".Management") + ) { + errorOutput += `Invalid folder structure: TypeSpec for data-plane specs or shared code must be in a folder NOT ending with '.Management'`; + success = false; + } + } + return { success: success, stdOutput: stdOutput, diff --git a/eng/tools/typespec-validation/test/folder-structure.test.ts b/eng/tools/typespec-validation/test/folder-structure.test.ts index e1879ead200e..300788ad5f75 100644 --- a/eng/tools/typespec-validation/test/folder-structure.test.ts +++ b/eng/tools/typespec-validation/test/folder-structure.test.ts @@ -205,4 +205,94 @@ describe("folder-structure", function () { assert(result.errorOutput); assert(result.errorOutput.includes("must contain")); }); + + it("should succeed with resource-manager/Management", async function() { + let host = new TsvTestHost(); + host.globby = async () => { + return ["/foo/Foo.Management/tspconfig.yaml"]; + }; + host.normalizePath = () => { + return "/gitroot"; + }; + host.readTspConfig = async (_folder: string) => ` +options: + "@azure-tools/typespec-autorest": + azure-resource-provider-folder: "resource-manager" +`; + + const result = await new FolderStructureRule().execute( + host, + "/gitroot/specification/foo/Foo.Management", + ); + + assert(result.success); + }); + + it("should succeed with data-plane/NoManagement", async function() { + let host = new TsvTestHost(); + host.globby = async () => { + return ["/foo/Foo/tspconfig.yaml"]; + }; + host.normalizePath = () => { + return "/gitroot"; + }; + host.readTspConfig = async (_folder: string) => ` +options: + "@azure-tools/typespec-autorest": + azure-resource-provider-folder: "data-plane" +`; + + const result = await new FolderStructureRule().execute( + host, + "/gitroot/specification/foo/Foo", + ); + + assert(result.success); + }); + + it("should fail with resource-manager/NoManagement", async function() { + let host = new TsvTestHost(); + host.globby = async () => { + return ["/foo/Foo/tspconfig.yaml"]; + }; + host.normalizePath = () => { + return "/gitroot"; + }; + host.readTspConfig = async (_folder: string) => ` +options: + "@azure-tools/typespec-autorest": + azure-resource-provider-folder: "resource-manager" +`; + + const result = await new FolderStructureRule().execute( + host, + "/gitroot/specification/foo/Foo", + ); + + assert(result.errorOutput); + assert(result.errorOutput.includes(".Management")); + }); + + it("should fail with data-plane/Management", async function() { + let host = new TsvTestHost(); + host.globby = async () => { + return ["/foo/Foo.Management/tspconfig.yaml"]; + }; + host.normalizePath = () => { + return "/gitroot"; + }; + host.readTspConfig = async (_folder: string) => ` +options: + "@azure-tools/typespec-autorest": + azure-resource-provider-folder: "data-plane" +`; + + const result = await new FolderStructureRule().execute( + host, + "/gitroot/specification/foo/Foo.Management", + ); + + assert(result.errorOutput); + assert(result.errorOutput.includes(".Management")); + }); });