From 92e5232e666159d090db3252831ba1e8bc95d201 Mon Sep 17 00:00:00 2001 From: Allen Zhang Date: Fri, 27 Sep 2024 21:01:07 -0700 Subject: [PATCH 1/4] Add support for loading example files for nested folders --- .../nexted-examples/examples/read/read.json | 14 +++ .../nexted-examples/examples/write/write.json | 14 +++ .../specs/misc/nexted-examples/main.tsp | 18 ++++ .../specs/misc/nexted-examples/tspconfig.yaml | 2 + .../typespec-autorest/examples/read/read.json | 14 +++ .../examples/write/write.json | 14 +++ .../typespec-autorest/openapi.json | 85 +++++++++++++++++++ packages/typespec-autorest/src/openapi.ts | 28 +++++- .../test/x-ms-examples.test.ts | 34 ++++++++ 9 files changed, 222 insertions(+), 1 deletion(-) create mode 100644 packages/samples/specs/misc/nexted-examples/examples/read/read.json create mode 100644 packages/samples/specs/misc/nexted-examples/examples/write/write.json create mode 100644 packages/samples/specs/misc/nexted-examples/main.tsp create mode 100644 packages/samples/specs/misc/nexted-examples/tspconfig.yaml create mode 100644 packages/samples/test/output/azure/core/misc/nexted-examples/@azure-tools/typespec-autorest/examples/read/read.json create mode 100644 packages/samples/test/output/azure/core/misc/nexted-examples/@azure-tools/typespec-autorest/examples/write/write.json create mode 100644 packages/samples/test/output/azure/core/misc/nexted-examples/@azure-tools/typespec-autorest/openapi.json diff --git a/packages/samples/specs/misc/nexted-examples/examples/read/read.json b/packages/samples/specs/misc/nexted-examples/examples/read/read.json new file mode 100644 index 0000000000..d5c85a585e --- /dev/null +++ b/packages/samples/specs/misc/nexted-examples/examples/read/read.json @@ -0,0 +1,14 @@ +{ + "title": "Read pet", + "operationId": "Pets_Read", + "description": "Read pet example", + "parameters": {}, + "responses": { + "200": { + "body": { + "name": "Rex", + "age": 3 + } + } + } +} diff --git a/packages/samples/specs/misc/nexted-examples/examples/write/write.json b/packages/samples/specs/misc/nexted-examples/examples/write/write.json new file mode 100644 index 0000000000..9bbead55a9 --- /dev/null +++ b/packages/samples/specs/misc/nexted-examples/examples/write/write.json @@ -0,0 +1,14 @@ +{ + "title": "Write pet", + "operationId": "Pets_Write", + "description": "Read pet example", + "parameters": { + "body": { + "name": "Rex", + "age": 3 + } + }, + "responses": { + "200": {} + } +} diff --git a/packages/samples/specs/misc/nexted-examples/main.tsp b/packages/samples/specs/misc/nexted-examples/main.tsp new file mode 100644 index 0000000000..7524ee9344 --- /dev/null +++ b/packages/samples/specs/misc/nexted-examples/main.tsp @@ -0,0 +1,18 @@ +import "@typespec/http"; + +using Http; + +@service +namespace XmsExamples; + +model Pet { + name: string; + age: int32; +} + +interface Pets { + // This operation should automatically get the ./examples/read.json file connected in x-ms-examples + read(): Pet; + // This operation should automatically get the ./examples/write.json file connected in x-ms-examples + write(@body pet: Pet): void; +} diff --git a/packages/samples/specs/misc/nexted-examples/tspconfig.yaml b/packages/samples/specs/misc/nexted-examples/tspconfig.yaml new file mode 100644 index 0000000000..e078dbe156 --- /dev/null +++ b/packages/samples/specs/misc/nexted-examples/tspconfig.yaml @@ -0,0 +1,2 @@ +emit: + - "@azure-tools/typespec-autorest" diff --git a/packages/samples/test/output/azure/core/misc/nexted-examples/@azure-tools/typespec-autorest/examples/read/read.json b/packages/samples/test/output/azure/core/misc/nexted-examples/@azure-tools/typespec-autorest/examples/read/read.json new file mode 100644 index 0000000000..d5c85a585e --- /dev/null +++ b/packages/samples/test/output/azure/core/misc/nexted-examples/@azure-tools/typespec-autorest/examples/read/read.json @@ -0,0 +1,14 @@ +{ + "title": "Read pet", + "operationId": "Pets_Read", + "description": "Read pet example", + "parameters": {}, + "responses": { + "200": { + "body": { + "name": "Rex", + "age": 3 + } + } + } +} diff --git a/packages/samples/test/output/azure/core/misc/nexted-examples/@azure-tools/typespec-autorest/examples/write/write.json b/packages/samples/test/output/azure/core/misc/nexted-examples/@azure-tools/typespec-autorest/examples/write/write.json new file mode 100644 index 0000000000..9bbead55a9 --- /dev/null +++ b/packages/samples/test/output/azure/core/misc/nexted-examples/@azure-tools/typespec-autorest/examples/write/write.json @@ -0,0 +1,14 @@ +{ + "title": "Write pet", + "operationId": "Pets_Write", + "description": "Read pet example", + "parameters": { + "body": { + "name": "Rex", + "age": 3 + } + }, + "responses": { + "200": {} + } +} diff --git a/packages/samples/test/output/azure/core/misc/nexted-examples/@azure-tools/typespec-autorest/openapi.json b/packages/samples/test/output/azure/core/misc/nexted-examples/@azure-tools/typespec-autorest/openapi.json new file mode 100644 index 0000000000..8715c10416 --- /dev/null +++ b/packages/samples/test/output/azure/core/misc/nexted-examples/@azure-tools/typespec-autorest/openapi.json @@ -0,0 +1,85 @@ +{ + "swagger": "2.0", + "info": { + "title": "(title)", + "version": "0000-00-00", + "x-typespec-generated": [ + { + "emitter": "@azure-tools/typespec-autorest" + } + ] + }, + "schemes": [ + "https" + ], + "produces": [ + "application/json" + ], + "consumes": [ + "application/json" + ], + "tags": [], + "paths": { + "/": { + "get": { + "operationId": "Pets_Read", + "parameters": [], + "responses": { + "200": { + "description": "The request has succeeded.", + "schema": { + "$ref": "#/definitions/Pet" + } + } + }, + "x-ms-examples": { + "Read pet": { + "$ref": "./examples/read/read.json" + } + } + }, + "post": { + "operationId": "Pets_Write", + "parameters": [ + { + "name": "pet", + "in": "body", + "required": true, + "schema": { + "$ref": "#/definitions/Pet" + } + } + ], + "responses": { + "204": { + "description": "There is no content to send for this request, but the headers may be useful. " + } + }, + "x-ms-examples": { + "Write pet": { + "$ref": "./examples/write/write.json" + } + } + } + } + }, + "definitions": { + "Pet": { + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "age": { + "type": "integer", + "format": "int32" + } + }, + "required": [ + "name", + "age" + ] + } + }, + "parameters": {} +} diff --git a/packages/typespec-autorest/src/openapi.ts b/packages/typespec-autorest/src/openapi.ts index e1a9f9fdfd..bc086c1b7a 100644 --- a/packages/typespec-autorest/src/openapi.ts +++ b/packages/typespec-autorest/src/openapi.ts @@ -88,6 +88,7 @@ import { isTemplateDeclarationOrInstance, isVoidType, navigateTypesInNamespace, + normalizePath, reportDeprecated, resolveEncodedName, resolvePath, @@ -133,6 +134,7 @@ import { shouldInline, } from "@typespec/openapi"; import { getVersionsForEnum } from "@typespec/versioning"; +import * as path from "path"; import { AutorestOpenAPISchema } from "./autorest-openapi-schema.js"; import { getExamples, getRef } from "./decorators.js"; import { sortWithJsonSchema } from "./json-schema-sorter/sorter.js"; @@ -2554,6 +2556,30 @@ async function checkExamplesDirExists(host: CompilerHost, dir: string) { } } +async function searchExampleJsonFiles(program: Program, exampleDir: string): Promise { + const host = program.host; + const exampleFiles: string[] = []; + + // Recursive file search + async function recursiveSearch(dir: string): Promise { + const fileItems = await host.readDir(dir); + + for (const item of fileItems) { + const relativePath = path.relative(exampleDir, path.join(dir, item)); + const fullPath = path.join(dir, item); + + if ((await host.stat(fullPath)).isDirectory()) { + await recursiveSearch(fullPath); + } else if ((await host.stat(fullPath)).isFile() && path.extname(item) === ".json") { + exampleFiles.push(normalizePath(relativePath)); + } + } + } + + await recursiveSearch(exampleDir); + return exampleFiles; +} + async function loadExamples( program: Program, options: AutorestDocumentEmitterOptions, @@ -2579,7 +2605,7 @@ async function loadExamples( } const map = new Map>(); - const exampleFiles = await host.readDir(exampleDir); + const exampleFiles = await searchExampleJsonFiles(program, exampleDir); for (const fileName of exampleFiles) { try { const exampleFile = await host.readFile(resolvePath(exampleDir, fileName)); diff --git a/packages/typespec-autorest/test/x-ms-examples.test.ts b/packages/typespec-autorest/test/x-ms-examples.test.ts index 944cd17c14..ee3dbea759 100644 --- a/packages/typespec-autorest/test/x-ms-examples.test.ts +++ b/packages/typespec-autorest/test/x-ms-examples.test.ts @@ -153,6 +153,21 @@ describe("explicit example", () => { expect(host.fs.has(resolveVirtualPath("./tsp-output/examples/getPet.json"))).toBe(true); }); + it("read nested examples from {project-root}/examples", async () => { + addExampleFile("./examples/pets/getPet.json", { operationId: "Pets_get", title: "Get a pet" }); + + const openapi = await compileOpenAPI(`@operationId("Pets_get") op read(): void;`, { + host, + }); + + deepStrictEqual(openapi.paths["/"]?.get?.["x-ms-examples"], { + "Get a pet": { + $ref: "./examples/pets/getPet.json", + }, + }); + expect(host.fs.has(resolveVirtualPath("./tsp-output/examples/pets/getPet.json"))).toBe(true); + }); + it("read examples from examples-dir", async () => { addExampleFile("./my-examples/getPet.json", { operationId: "Pets_get", title: "Get a pet" }); @@ -169,6 +184,25 @@ describe("explicit example", () => { expect(host.fs.has(resolveVirtualPath("./tsp-output/examples/getPet.json"))).toBe(true); }); + it("read nested examples from examples-dir", async () => { + addExampleFile("./my-examples/pets/getPet.json", { + operationId: "Pets_get", + title: "Get a pet", + }); + + const openapi = await compileOpenAPI(`@operationId("Pets_get") op read(): void;`, { + host, + options: { "examples-dir": resolveVirtualPath("./my-examples") }, + }); + + deepStrictEqual(openapi.paths["/"]?.get?.["x-ms-examples"], { + "Get a pet": { + $ref: "./examples/pets/getPet.json", + }, + }); + expect(host.fs.has(resolveVirtualPath("./tsp-output/examples/pets/getPet.json"))).toBe(true); + }); + it("emit diagnostic when example files use same operation id", async () => { const runner = await createAutorestTestRunner(host, { "examples-dir": resolveVirtualPath("./examples"), From 058bebbf79ca74da59762752e6fb561580e6de0a Mon Sep 17 00:00:00 2001 From: Allen Zhang Date: Fri, 27 Sep 2024 21:02:37 -0700 Subject: [PATCH 2/4] changelog --- .../changes/azhang_NestedExamples-2024-8-27-21-2-20.md | 7 +++++++ 1 file changed, 7 insertions(+) create mode 100644 .chronus/changes/azhang_NestedExamples-2024-8-27-21-2-20.md diff --git a/.chronus/changes/azhang_NestedExamples-2024-8-27-21-2-20.md b/.chronus/changes/azhang_NestedExamples-2024-8-27-21-2-20.md new file mode 100644 index 0000000000..177d43a7c9 --- /dev/null +++ b/.chronus/changes/azhang_NestedExamples-2024-8-27-21-2-20.md @@ -0,0 +1,7 @@ +--- +changeKind: feature +packages: + - "@azure-tools/typespec-autorest" +--- + +Adding support for loading example files from nested sub-folders. \ No newline at end of file From adfe4181e3c5672d3b255c25cc0e4f579024cf5e Mon Sep 17 00:00:00 2001 From: Allen Zhang Date: Mon, 30 Sep 2024 10:42:33 -0700 Subject: [PATCH 3/4] Reorg test folder --- .../examples/read => x-ms-examples/flat/examples}/read.json | 0 .../examples/write => x-ms-examples/flat/examples}/write.json | 0 .../specs/misc/{nexted-examples => x-ms-examples/flat}/main.tsp | 0 .../misc/{nexted-examples => x-ms-examples/flat}/tspconfig.yaml | 0 .../x-ms-examples/{examples => nested/examples/read}/read.json | 0 .../x-ms-examples/{examples => nested/examples/write}/write.json | 0 packages/samples/specs/misc/x-ms-examples/{ => nested}/main.tsp | 0 .../samples/specs/misc/x-ms-examples/{ => nested}/tspconfig.yaml | 0 .../flat/@azure-tools/typespec-autorest/examples}/read.json | 0 .../flat/@azure-tools/typespec-autorest/examples}/write.json | 0 .../{ => flat}/@azure-tools/typespec-autorest/openapi.json | 0 .../@azure-tools/typespec-autorest/examples/read}/read.json | 0 .../@azure-tools/typespec-autorest/examples/write}/write.json | 0 .../nested}/@azure-tools/typespec-autorest/openapi.json | 0 14 files changed, 0 insertions(+), 0 deletions(-) rename packages/samples/specs/misc/{nexted-examples/examples/read => x-ms-examples/flat/examples}/read.json (100%) rename packages/samples/specs/misc/{nexted-examples/examples/write => x-ms-examples/flat/examples}/write.json (100%) rename packages/samples/specs/misc/{nexted-examples => x-ms-examples/flat}/main.tsp (100%) rename packages/samples/specs/misc/{nexted-examples => x-ms-examples/flat}/tspconfig.yaml (100%) rename packages/samples/specs/misc/x-ms-examples/{examples => nested/examples/read}/read.json (100%) rename packages/samples/specs/misc/x-ms-examples/{examples => nested/examples/write}/write.json (100%) rename packages/samples/specs/misc/x-ms-examples/{ => nested}/main.tsp (100%) rename packages/samples/specs/misc/x-ms-examples/{ => nested}/tspconfig.yaml (100%) rename packages/samples/test/output/azure/core/misc/{nexted-examples/@azure-tools/typespec-autorest/examples/read => x-ms-examples/flat/@azure-tools/typespec-autorest/examples}/read.json (100%) rename packages/samples/test/output/azure/core/misc/{nexted-examples/@azure-tools/typespec-autorest/examples/write => x-ms-examples/flat/@azure-tools/typespec-autorest/examples}/write.json (100%) rename packages/samples/test/output/azure/core/misc/x-ms-examples/{ => flat}/@azure-tools/typespec-autorest/openapi.json (100%) rename packages/samples/test/output/azure/core/misc/x-ms-examples/{@azure-tools/typespec-autorest/examples => nested/@azure-tools/typespec-autorest/examples/read}/read.json (100%) rename packages/samples/test/output/azure/core/misc/x-ms-examples/{@azure-tools/typespec-autorest/examples => nested/@azure-tools/typespec-autorest/examples/write}/write.json (100%) rename packages/samples/test/output/azure/core/misc/{nexted-examples => x-ms-examples/nested}/@azure-tools/typespec-autorest/openapi.json (100%) diff --git a/packages/samples/specs/misc/nexted-examples/examples/read/read.json b/packages/samples/specs/misc/x-ms-examples/flat/examples/read.json similarity index 100% rename from packages/samples/specs/misc/nexted-examples/examples/read/read.json rename to packages/samples/specs/misc/x-ms-examples/flat/examples/read.json diff --git a/packages/samples/specs/misc/nexted-examples/examples/write/write.json b/packages/samples/specs/misc/x-ms-examples/flat/examples/write.json similarity index 100% rename from packages/samples/specs/misc/nexted-examples/examples/write/write.json rename to packages/samples/specs/misc/x-ms-examples/flat/examples/write.json diff --git a/packages/samples/specs/misc/nexted-examples/main.tsp b/packages/samples/specs/misc/x-ms-examples/flat/main.tsp similarity index 100% rename from packages/samples/specs/misc/nexted-examples/main.tsp rename to packages/samples/specs/misc/x-ms-examples/flat/main.tsp diff --git a/packages/samples/specs/misc/nexted-examples/tspconfig.yaml b/packages/samples/specs/misc/x-ms-examples/flat/tspconfig.yaml similarity index 100% rename from packages/samples/specs/misc/nexted-examples/tspconfig.yaml rename to packages/samples/specs/misc/x-ms-examples/flat/tspconfig.yaml diff --git a/packages/samples/specs/misc/x-ms-examples/examples/read.json b/packages/samples/specs/misc/x-ms-examples/nested/examples/read/read.json similarity index 100% rename from packages/samples/specs/misc/x-ms-examples/examples/read.json rename to packages/samples/specs/misc/x-ms-examples/nested/examples/read/read.json diff --git a/packages/samples/specs/misc/x-ms-examples/examples/write.json b/packages/samples/specs/misc/x-ms-examples/nested/examples/write/write.json similarity index 100% rename from packages/samples/specs/misc/x-ms-examples/examples/write.json rename to packages/samples/specs/misc/x-ms-examples/nested/examples/write/write.json diff --git a/packages/samples/specs/misc/x-ms-examples/main.tsp b/packages/samples/specs/misc/x-ms-examples/nested/main.tsp similarity index 100% rename from packages/samples/specs/misc/x-ms-examples/main.tsp rename to packages/samples/specs/misc/x-ms-examples/nested/main.tsp diff --git a/packages/samples/specs/misc/x-ms-examples/tspconfig.yaml b/packages/samples/specs/misc/x-ms-examples/nested/tspconfig.yaml similarity index 100% rename from packages/samples/specs/misc/x-ms-examples/tspconfig.yaml rename to packages/samples/specs/misc/x-ms-examples/nested/tspconfig.yaml diff --git a/packages/samples/test/output/azure/core/misc/nexted-examples/@azure-tools/typespec-autorest/examples/read/read.json b/packages/samples/test/output/azure/core/misc/x-ms-examples/flat/@azure-tools/typespec-autorest/examples/read.json similarity index 100% rename from packages/samples/test/output/azure/core/misc/nexted-examples/@azure-tools/typespec-autorest/examples/read/read.json rename to packages/samples/test/output/azure/core/misc/x-ms-examples/flat/@azure-tools/typespec-autorest/examples/read.json diff --git a/packages/samples/test/output/azure/core/misc/nexted-examples/@azure-tools/typespec-autorest/examples/write/write.json b/packages/samples/test/output/azure/core/misc/x-ms-examples/flat/@azure-tools/typespec-autorest/examples/write.json similarity index 100% rename from packages/samples/test/output/azure/core/misc/nexted-examples/@azure-tools/typespec-autorest/examples/write/write.json rename to packages/samples/test/output/azure/core/misc/x-ms-examples/flat/@azure-tools/typespec-autorest/examples/write.json diff --git a/packages/samples/test/output/azure/core/misc/x-ms-examples/@azure-tools/typespec-autorest/openapi.json b/packages/samples/test/output/azure/core/misc/x-ms-examples/flat/@azure-tools/typespec-autorest/openapi.json similarity index 100% rename from packages/samples/test/output/azure/core/misc/x-ms-examples/@azure-tools/typespec-autorest/openapi.json rename to packages/samples/test/output/azure/core/misc/x-ms-examples/flat/@azure-tools/typespec-autorest/openapi.json diff --git a/packages/samples/test/output/azure/core/misc/x-ms-examples/@azure-tools/typespec-autorest/examples/read.json b/packages/samples/test/output/azure/core/misc/x-ms-examples/nested/@azure-tools/typespec-autorest/examples/read/read.json similarity index 100% rename from packages/samples/test/output/azure/core/misc/x-ms-examples/@azure-tools/typespec-autorest/examples/read.json rename to packages/samples/test/output/azure/core/misc/x-ms-examples/nested/@azure-tools/typespec-autorest/examples/read/read.json diff --git a/packages/samples/test/output/azure/core/misc/x-ms-examples/@azure-tools/typespec-autorest/examples/write.json b/packages/samples/test/output/azure/core/misc/x-ms-examples/nested/@azure-tools/typespec-autorest/examples/write/write.json similarity index 100% rename from packages/samples/test/output/azure/core/misc/x-ms-examples/@azure-tools/typespec-autorest/examples/write.json rename to packages/samples/test/output/azure/core/misc/x-ms-examples/nested/@azure-tools/typespec-autorest/examples/write/write.json diff --git a/packages/samples/test/output/azure/core/misc/nexted-examples/@azure-tools/typespec-autorest/openapi.json b/packages/samples/test/output/azure/core/misc/x-ms-examples/nested/@azure-tools/typespec-autorest/openapi.json similarity index 100% rename from packages/samples/test/output/azure/core/misc/nexted-examples/@azure-tools/typespec-autorest/openapi.json rename to packages/samples/test/output/azure/core/misc/x-ms-examples/nested/@azure-tools/typespec-autorest/openapi.json From f2e44f014e543c5bdfccda67419214826f92547c Mon Sep 17 00:00:00 2001 From: Allen Zhang Date: Mon, 30 Sep 2024 11:03:04 -0700 Subject: [PATCH 4/4] Remove Path reference and replace with compiler functions --- packages/typespec-autorest/src/openapi.ts | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/packages/typespec-autorest/src/openapi.ts b/packages/typespec-autorest/src/openapi.ts index bc086c1b7a..a9f0f183b3 100644 --- a/packages/typespec-autorest/src/openapi.ts +++ b/packages/typespec-autorest/src/openapi.ts @@ -50,6 +50,7 @@ import { createDiagnosticCollector, explainStringTemplateNotSerializable, getAllTags, + getAnyExtensionFromPath, getDirectoryPath, getDiscriminator, getDoc, @@ -87,6 +88,7 @@ import { isTemplateDeclaration, isTemplateDeclarationOrInstance, isVoidType, + joinPaths, navigateTypesInNamespace, normalizePath, reportDeprecated, @@ -134,7 +136,6 @@ import { shouldInline, } from "@typespec/openapi"; import { getVersionsForEnum } from "@typespec/versioning"; -import * as path from "path"; import { AutorestOpenAPISchema } from "./autorest-openapi-schema.js"; import { getExamples, getRef } from "./decorators.js"; import { sortWithJsonSchema } from "./json-schema-sorter/sorter.js"; @@ -2565,12 +2566,15 @@ async function searchExampleJsonFiles(program: Program, exampleDir: string): Pro const fileItems = await host.readDir(dir); for (const item of fileItems) { - const relativePath = path.relative(exampleDir, path.join(dir, item)); - const fullPath = path.join(dir, item); + const fullPath = joinPaths(dir, item); + const relativePath = getRelativePathFromDirectory(exampleDir, fullPath, false); if ((await host.stat(fullPath)).isDirectory()) { await recursiveSearch(fullPath); - } else if ((await host.stat(fullPath)).isFile() && path.extname(item) === ".json") { + } else if ( + (await host.stat(fullPath)).isFile() && + getAnyExtensionFromPath(item) === ".json" + ) { exampleFiles.push(normalizePath(relativePath)); } }