From 5356a9697e41dcbde3960b6434383bf4be46d5b7 Mon Sep 17 00:00:00 2001 From: albertxavier100 Date: Fri, 7 Feb 2025 18:26:12 +0800 Subject: [PATCH 01/25] fix --- packages/openapi3/src/lib.ts | 2 +- packages/openapi3/src/openapi.ts | 21 ++------------------- packages/openapi3/src/schema-emitter.ts | 3 ++- packages/openapi3/src/util.ts | 17 +++++++++++++++++ packages/openapi3/test/models.test.ts | 8 ++++++++ 5 files changed, 30 insertions(+), 21 deletions(-) diff --git a/packages/openapi3/src/lib.ts b/packages/openapi3/src/lib.ts index 7ea2e95cf9..a6edf7e9ae 100644 --- a/packages/openapi3/src/lib.ts +++ b/packages/openapi3/src/lib.ts @@ -299,7 +299,7 @@ export const libDef = { }, }, "invalid-component-fixed-field-key": { - severity: "error", + severity: "warning", messages: { default: paramMessage`Invalid key '${"value"}' used in a fixed field of the Component object. Only alphanumerics, dot (.), hyphen (-), and underscore (_) characters are allowed in keys.`, }, diff --git a/packages/openapi3/src/openapi.ts b/packages/openapi3/src/openapi.ts index 663d62c549..3fd23e15df 100644 --- a/packages/openapi3/src/openapi.ts +++ b/packages/openapi3/src/openapi.ts @@ -119,6 +119,7 @@ import { isBytesKeptRaw, isSharedHttpOperation, SharedHttpOperation, + validateComponentFixedFieldKey, } from "./util.js"; import { resolveVisibilityUsage, VisibilityUsageTracker } from "./visibility-usage.js"; import { resolveXmlModule, XmlModule } from "./xml-module.js"; @@ -1560,21 +1561,6 @@ function createOAPIEmitter( } } - function validateComponentFixedFieldKey(type: Type, name: string) { - const pattern = /^[a-zA-Z0-9.\-_]+$/; - if (!pattern.test(name)) { - program.reportDiagnostic( - createDiagnostic({ - code: "invalid-component-fixed-field-key", - format: { - value: name, - }, - target: type, - }), - ); - } - } - function emitParameters() { for (const [property, param] of params) { const key = getParameterKey( @@ -1584,8 +1570,7 @@ function createOAPIEmitter( root.components!.parameters!, typeNameOptions, ); - validateComponentFixedFieldKey(property, key); - + validateComponentFixedFieldKey(program, property, key); root.components!.parameters![key] = { ...param }; for (const key of Object.keys(param)) { delete param[key]; @@ -1609,8 +1594,6 @@ function createOAPIEmitter( const schemas = root.components!.schemas!; const declarations = files[0].globalScope.declarations; for (const declaration of declarations) { - validateComponentFixedFieldKey(serviceNamespace, declaration.name); - schemas[declaration.name] = declaration.value as any; } } diff --git a/packages/openapi3/src/schema-emitter.ts b/packages/openapi3/src/schema-emitter.ts index ace487d424..1cf780c699 100644 --- a/packages/openapi3/src/schema-emitter.ts +++ b/packages/openapi3/src/schema-emitter.ts @@ -75,7 +75,7 @@ import { OpenAPI3SchemaProperty, OpenAPISchema3_1, } from "./types.js"; -import { getDefaultValue, includeDerivedModel, isBytesKeptRaw, isStdType } from "./util.js"; +import { getDefaultValue, includeDerivedModel, isBytesKeptRaw, isStdType, validateComponentFixedFieldKey } from "./util.js"; import { VisibilityUsageTracker } from "./visibility-usage.js"; import { XmlModule } from "./xml-module.js"; @@ -157,6 +157,7 @@ export class OpenAPI3SchemaEmitterBase< modelDeclaration(model: Model, _: string): EmitterOutput { const program = this.emitter.getProgram(); + validateComponentFixedFieldKey(program, model, model.name); const visibility = this.#getVisibilityContext(); const schema: ObjectBuilder = new ObjectBuilder({ type: "object", diff --git a/packages/openapi3/src/util.ts b/packages/openapi3/src/util.ts index 929be66094..2462294a5e 100644 --- a/packages/openapi3/src/util.ts +++ b/packages/openapi3/src/util.ts @@ -14,6 +14,7 @@ import { Value, } from "@typespec/compiler"; import { HttpOperation } from "@typespec/http"; +import { createDiagnostic } from "./lib.js"; /** * Checks if two objects are deeply equal. * @@ -158,3 +159,19 @@ export function getDefaultValue( export function isBytesKeptRaw(program: Program, type: Type) { return type.kind === "Scalar" && type.name === "bytes" && getEncode(program, type) === undefined; } + + +export function validateComponentFixedFieldKey(program: Program, type: Type, name: string) { + const pattern = /^[a-zA-Z0-9.\-_]+$/; + if (!pattern.test(name)) { + program.reportDiagnostic( + createDiagnostic({ + code: "invalid-component-fixed-field-key", + format: { + value: name, + }, + target: type, + }), + ); + } +} diff --git a/packages/openapi3/test/models.test.ts b/packages/openapi3/test/models.test.ts index cbaf257ebe..14ce95fea1 100644 --- a/packages/openapi3/test/models.test.ts +++ b/packages/openapi3/test/models.test.ts @@ -2,6 +2,7 @@ import { expectDiagnostics } from "@typespec/compiler/testing"; import { deepStrictEqual, ok, strictEqual } from "assert"; import { describe, expect, it } from "vitest"; import { worksFor } from "./works-for.js"; +import { DiagnosticTarget } from "@typespec/compiler"; worksFor(["3.0.0", "3.1.0"], ({ diagnoseOpenApiFor, oapiForModel, openApiFor }) => { it("defines models", async () => { @@ -141,6 +142,13 @@ worksFor(["3.0.0", "3.1.0"], ({ diagnoseOpenApiFor, oapiForModel, openApiFor }) code: "@typespec/openapi3/invalid-component-fixed-field-key", }, ]); + diagnostics.forEach((d) => { + const diagnosticTarget = d.target as DiagnosticTarget; + strictEqual( + diagnosticTarget && "kind" in diagnosticTarget && diagnosticTarget.kind === "Model", + true, + ); + }); }); }); From cf53b00f7f110da57ca4c8aa6289696b75a3dc61 Mon Sep 17 00:00:00 2001 From: albertxavier100 Date: Fri, 7 Feb 2025 19:05:04 +0800 Subject: [PATCH 02/25] fix checks --- .chronus/changes/wanl-fix-diag-1-2025-1-7-19-3-44.md | 7 +++++++ packages/openapi3/src/schema-emitter.ts | 8 +++++++- packages/openapi3/src/util.ts | 1 - packages/openapi3/test/models.test.ts | 2 +- 4 files changed, 15 insertions(+), 3 deletions(-) create mode 100644 .chronus/changes/wanl-fix-diag-1-2025-1-7-19-3-44.md diff --git a/.chronus/changes/wanl-fix-diag-1-2025-1-7-19-3-44.md b/.chronus/changes/wanl-fix-diag-1-2025-1-7-19-3-44.md new file mode 100644 index 0000000000..9cd48a4ced --- /dev/null +++ b/.chronus/changes/wanl-fix-diag-1-2025-1-7-19-3-44.md @@ -0,0 +1,7 @@ +--- +changeKind: fix +packages: + - "@typespec/openapi3" +--- + +Fix: @typespec/openapi3/invalid-component-fixed-field-key show on incorrect target \ No newline at end of file diff --git a/packages/openapi3/src/schema-emitter.ts b/packages/openapi3/src/schema-emitter.ts index 1cf780c699..df36028ce4 100644 --- a/packages/openapi3/src/schema-emitter.ts +++ b/packages/openapi3/src/schema-emitter.ts @@ -75,7 +75,13 @@ import { OpenAPI3SchemaProperty, OpenAPISchema3_1, } from "./types.js"; -import { getDefaultValue, includeDerivedModel, isBytesKeptRaw, isStdType, validateComponentFixedFieldKey } from "./util.js"; +import { + getDefaultValue, + includeDerivedModel, + isBytesKeptRaw, + isStdType, + validateComponentFixedFieldKey, +} from "./util.js"; import { VisibilityUsageTracker } from "./visibility-usage.js"; import { XmlModule } from "./xml-module.js"; diff --git a/packages/openapi3/src/util.ts b/packages/openapi3/src/util.ts index 2462294a5e..4ff361d074 100644 --- a/packages/openapi3/src/util.ts +++ b/packages/openapi3/src/util.ts @@ -160,7 +160,6 @@ export function isBytesKeptRaw(program: Program, type: Type) { return type.kind === "Scalar" && type.name === "bytes" && getEncode(program, type) === undefined; } - export function validateComponentFixedFieldKey(program: Program, type: Type, name: string) { const pattern = /^[a-zA-Z0-9.\-_]+$/; if (!pattern.test(name)) { diff --git a/packages/openapi3/test/models.test.ts b/packages/openapi3/test/models.test.ts index 14ce95fea1..e6a324438e 100644 --- a/packages/openapi3/test/models.test.ts +++ b/packages/openapi3/test/models.test.ts @@ -1,8 +1,8 @@ +import { DiagnosticTarget } from "@typespec/compiler"; import { expectDiagnostics } from "@typespec/compiler/testing"; import { deepStrictEqual, ok, strictEqual } from "assert"; import { describe, expect, it } from "vitest"; import { worksFor } from "./works-for.js"; -import { DiagnosticTarget } from "@typespec/compiler"; worksFor(["3.0.0", "3.1.0"], ({ diagnoseOpenApiFor, oapiForModel, openApiFor }) => { it("defines models", async () => { From 31bde6ceb8558c5936781f574754b7340f0a1407 Mon Sep 17 00:00:00 2001 From: albertxavier100 Date: Sat, 8 Feb 2025 10:48:08 +0800 Subject: [PATCH 03/25] . --- .chronus/changes/wanl-fix-diag-1-2025-1-7-19-3-44.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.chronus/changes/wanl-fix-diag-1-2025-1-7-19-3-44.md b/.chronus/changes/wanl-fix-diag-1-2025-1-7-19-3-44.md index 9cd48a4ced..8231524dc8 100644 --- a/.chronus/changes/wanl-fix-diag-1-2025-1-7-19-3-44.md +++ b/.chronus/changes/wanl-fix-diag-1-2025-1-7-19-3-44.md @@ -4,4 +4,4 @@ packages: - "@typespec/openapi3" --- -Fix: @typespec/openapi3/invalid-component-fixed-field-key show on incorrect target \ No newline at end of file +Fix: `@typespec/openapi3/invalid-component-fixed-field-key` show on incorrect target From f1fd53e1593fa435003628a728104a4762f081c9 Mon Sep 17 00:00:00 2001 From: albertxavier100 Date: Sat, 8 Feb 2025 12:12:39 +0800 Subject: [PATCH 04/25] add removeInvalidCharactersInComponentFixedFiledKey --- packages/openapi3/src/util.ts | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/packages/openapi3/src/util.ts b/packages/openapi3/src/util.ts index 4ff361d074..d556d2547d 100644 --- a/packages/openapi3/src/util.ts +++ b/packages/openapi3/src/util.ts @@ -160,7 +160,7 @@ export function isBytesKeptRaw(program: Program, type: Type) { return type.kind === "Scalar" && type.name === "bytes" && getEncode(program, type) === undefined; } -export function validateComponentFixedFieldKey(program: Program, type: Type, name: string) { +export function validateComponentFixedFieldKey(program: Program, type: Type, name: string): boolean { const pattern = /^[a-zA-Z0-9.\-_]+$/; if (!pattern.test(name)) { program.reportDiagnostic( @@ -172,5 +172,11 @@ export function validateComponentFixedFieldKey(program: Program, type: Type, nam target: type, }), ); + return false; } + return true; +} + +export function removeInvalidCharactersInComponentFixedFiledKey(key: string) { + return key.replace(/[^a-zA-Z0-9.\-_]/g, ""); } From add6d09d579ac9030e869f761ec93a50b0aecd58 Mon Sep 17 00:00:00 2001 From: albertxavier100 Date: Sat, 8 Feb 2025 12:13:10 +0800 Subject: [PATCH 05/25] Revert "add removeInvalidCharactersInComponentFixedFiledKey" This reverts commit f1fd53e1593fa435003628a728104a4762f081c9. --- packages/openapi3/src/util.ts | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/packages/openapi3/src/util.ts b/packages/openapi3/src/util.ts index d556d2547d..4ff361d074 100644 --- a/packages/openapi3/src/util.ts +++ b/packages/openapi3/src/util.ts @@ -160,7 +160,7 @@ export function isBytesKeptRaw(program: Program, type: Type) { return type.kind === "Scalar" && type.name === "bytes" && getEncode(program, type) === undefined; } -export function validateComponentFixedFieldKey(program: Program, type: Type, name: string): boolean { +export function validateComponentFixedFieldKey(program: Program, type: Type, name: string) { const pattern = /^[a-zA-Z0-9.\-_]+$/; if (!pattern.test(name)) { program.reportDiagnostic( @@ -172,11 +172,5 @@ export function validateComponentFixedFieldKey(program: Program, type: Type, nam target: type, }), ); - return false; } - return true; -} - -export function removeInvalidCharactersInComponentFixedFiledKey(key: string) { - return key.replace(/[^a-zA-Z0-9.\-_]/g, ""); } From b70098fa76c848931dca399ffcee0d51ec04eb1b Mon Sep 17 00:00:00 2001 From: albertxavier100 Date: Mon, 10 Feb 2025 09:38:00 +0800 Subject: [PATCH 06/25] WIP - do not review this commit --- packages/openapi3/src/openapi.ts | 66 +++++++++++++++++++++++++++++++- packages/openapi3/src/util.ts | 17 ++++++-- 2 files changed, 79 insertions(+), 4 deletions(-) diff --git a/packages/openapi3/src/openapi.ts b/packages/openapi3/src/openapi.ts index 3fd23e15df..d1f7ab9d3e 100644 --- a/packages/openapi3/src/openapi.ts +++ b/packages/openapi3/src/openapi.ts @@ -45,7 +45,6 @@ import { } from "@typespec/compiler"; import { AssetEmitter, createAssetEmitter, EmitEntity } from "@typespec/compiler/emitter-framework"; -import {} from "@typespec/compiler/utils"; import { AuthenticationOptionReference, AuthenticationReference, @@ -116,6 +115,7 @@ import { import { deepEquals, getDefaultValue, + invalidComponentFixedFieldKey, isBytesKeptRaw, isSharedHttpOperation, SharedHttpOperation, @@ -274,6 +274,10 @@ function createOAPIEmitter( }, }; + program + .stateMap(invalidComponentFixedFieldKey) + .set(program.getGlobalNamespaceType(), new Map()); + return { emitOpenAPI, getOpenAPI }; async function emitOpenAPI() { @@ -676,6 +680,13 @@ function createOAPIEmitter( } } + // Use valid keys in components/schemas and components/parameters + const invalidKeys = program + .stateMap(invalidComponentFixedFieldKey) + .get(program.getGlobalNamespaceType()) as Set; + useValidKeysInComponentFixedFields(invalidKeys, root.components?.schemas); + useValidKeysInComponentFixedFields(invalidKeys, root.components?.parameters); + return [root, diagnostics.diagnostics]; } catch (err) { if (err instanceof ErrorTypeFoundError) { @@ -686,6 +697,58 @@ function createOAPIEmitter( throw err; } } + + // TODO: discuss: should we update something x-ref: $ref: ignore it? what about x-ms-* + function updateRefs(root: SupportedOpenAPIDocuments, oldKey: string, newKey: string) { + + } + + function useValidKeysInComponentFixedFields(invalidKeys: Set, pairs?: Record): Map | undefined { + if (!pairs) return; + + const newPairs: typeof pairs = {}; + const originalKeys = Object.keys(pairs); + const validKeys = new Set(originalKeys.filter((key) => !invalidKeys.has(key))); + const replaceMap = new Map(); + + for (const [_, key] of originalKeys.entries()) { + if (validKeys.has(key)) { + newPairs[key] = pairs[key]; + continue; + } + + const newKey = createValidKey(key, validKeys, newPairs); + newPairs[newKey] = pairs[key]; + pairs = newPairs; + replaceMap.set(key, newKey); + } + + return replaceMap; + + function createValidKey( + invalidKey: string, + originalValidKeys: Set, + newPairs: Record, + ): string { + let baseKey = invalidKey.replace(/[^a-zA-Z0-9.\-_]/g, ""); + let index = 1; + let validKey = baseKey; + + // TODO: discuss: limit maximum retry? + const maxRetry = 1000000; + for ( + let validKey = baseKey; + index <= maxRetry && (validKey in originalValidKeys || validKey in newPairs); + index++ + ) { + validKey = baseKey + index; + } + if (index > maxRetry) { + // TODO: report diagnostic + } + return validKey; + } + } } function joinOps( @@ -814,6 +877,7 @@ function createOAPIEmitter( parameters: getEndpointParameters(parameters.parameters, visibility), responses: getResponses(operation, operation.responses, examples), }; + const currentTags = getAllTags(program, op); if (currentTags) { oai3Operation.tags = currentTags; diff --git a/packages/openapi3/src/util.ts b/packages/openapi3/src/util.ts index 4ff361d074..6f4f2b3562 100644 --- a/packages/openapi3/src/util.ts +++ b/packages/openapi3/src/util.ts @@ -14,7 +14,7 @@ import { Value, } from "@typespec/compiler"; import { HttpOperation } from "@typespec/http"; -import { createDiagnostic } from "./lib.js"; +import { createDiagnostic, createStateSymbol } from "./lib.js"; /** * Checks if two objects are deeply equal. * @@ -160,9 +160,15 @@ export function isBytesKeptRaw(program: Program, type: Type) { return type.kind === "Scalar" && type.name === "bytes" && getEncode(program, type) === undefined; } +export const invalidComponentFixedFieldKey = createStateSymbol("invalidComponentFixedField"); export function validateComponentFixedFieldKey(program: Program, type: Type, name: string) { - const pattern = /^[a-zA-Z0-9.\-_]+$/; - if (!pattern.test(name)) { + if (!isValidComponentFixedFieldKey) { + const invalidKeys = program + .stateMap(invalidComponentFixedFieldKey) + .get(program.getGlobalNamespaceType()) as Set; + if (!invalidKeys.has(name)) { + invalidKeys.add(name); + } program.reportDiagnostic( createDiagnostic({ code: "invalid-component-fixed-field-key", @@ -174,3 +180,8 @@ export function validateComponentFixedFieldKey(program: Program, type: Type, nam ); } } + +export function isValidComponentFixedFieldKey(key: string) { + const validPattern = /^[a-zA-Z0-9.\-_]+$/; + return validPattern.test(key); +} From e4e6938df55559b6cf0c85126849e53a22fabb6b Mon Sep 17 00:00:00 2001 From: Wanpeng Li Date: Wed, 12 Feb 2025 18:08:23 +0800 Subject: [PATCH 07/25] WIP: find anything uses references --- packages/openapi3/src/openapi.ts | 50 ++++++++++++++++++++++---------- 1 file changed, 35 insertions(+), 15 deletions(-) diff --git a/packages/openapi3/src/openapi.ts b/packages/openapi3/src/openapi.ts index d1f7ab9d3e..c88d6d3bfa 100644 --- a/packages/openapi3/src/openapi.ts +++ b/packages/openapi3/src/openapi.ts @@ -685,8 +685,14 @@ function createOAPIEmitter( .stateMap(invalidComponentFixedFieldKey) .get(program.getGlobalNamespaceType()) as Set; useValidKeysInComponentFixedFields(invalidKeys, root.components?.schemas); - useValidKeysInComponentFixedFields(invalidKeys, root.components?.parameters); - + const invalidToValidKeyMap = useValidKeysInComponentFixedFields( + invalidKeys, + root.components?.parameters, + ); + for (const invalidKey in invalidToValidKeyMap) { + const validKey = invalidToValidKeyMap.get(invalidKey); + updateRefs(root, invalidKey, validKey!); + } return [root, diagnostics.diagnostics]; } catch (err) { if (err instanceof ErrorTypeFoundError) { @@ -698,18 +704,37 @@ function createOAPIEmitter( } } - // TODO: discuss: should we update something x-ref: $ref: ignore it? what about x-ms-* - function updateRefs(root: SupportedOpenAPIDocuments, oldKey: string, newKey: string) { + + // find anything uses references + + // TODO: OpenAPI3SchemaProperty + // TODO: OpenAPI3Link + + // TODO: OpenAPI3Responses.status + // TODO: OpenAPI3Response.headers + // TODO: OpenAPI3Response.links + // TODO: OpenAPI3MediaType.schema + // TODO: OpenAPI3Encoding.headers + // TODO: OpenAPI3Schema.allof/oneof/anyof/not/items/additionalProperties/ + // TODO: OpenAPI3Operation.requestBody + // TODO: OpenAPI3Operation.parameters + + // TODO: JsonSchema.contentSchema/allof/anyof/oneof/not/items/prefixItems/contains/properties/additionalProperties + // TODO:OpenAPIComponents3_1.responses/parameters/examples/requestBodies/headers/securitySchemes/links + function updateRefs(root: SupportedOpenAPIDocuments, oldModelName: string, newModelName: string) { } - function useValidKeysInComponentFixedFields(invalidKeys: Set, pairs?: Record): Map | undefined { + function useValidKeysInComponentFixedFields( + invalidKeys: Set, + pairs?: Record, + ): Map | undefined { if (!pairs) return; const newPairs: typeof pairs = {}; const originalKeys = Object.keys(pairs); const validKeys = new Set(originalKeys.filter((key) => !invalidKeys.has(key))); - const replaceMap = new Map(); + const invalidToValidKeyMap = new Map(); for (const [_, key] of originalKeys.entries()) { if (validKeys.has(key)) { @@ -720,32 +745,27 @@ function createOAPIEmitter( const newKey = createValidKey(key, validKeys, newPairs); newPairs[newKey] = pairs[key]; pairs = newPairs; - replaceMap.set(key, newKey); + invalidToValidKeyMap.set(key, newKey); } - return replaceMap; + return invalidToValidKeyMap; function createValidKey( invalidKey: string, originalValidKeys: Set, newPairs: Record, ): string { - let baseKey = invalidKey.replace(/[^a-zA-Z0-9.\-_]/g, ""); + let baseKey = invalidKey.replace(/[^a-zA-Z0-9.\-_]/g, "_"); let index = 1; let validKey = baseKey; - // TODO: discuss: limit maximum retry? - const maxRetry = 1000000; for ( let validKey = baseKey; - index <= maxRetry && (validKey in originalValidKeys || validKey in newPairs); + validKey in originalValidKeys || validKey in newPairs; index++ ) { validKey = baseKey + index; } - if (index > maxRetry) { - // TODO: report diagnostic - } return validKey; } } From 46b30b78567fd7528e1b3328548307ba48720b65 Mon Sep 17 00:00:00 2001 From: Wanpeng Li Date: Wed, 19 Feb 2025 02:51:39 +0800 Subject: [PATCH 08/25] add test (todo: check ref) --- packages/openapi3/src/examples.ts | 2 +- packages/openapi3/src/openapi-helpers-3-1.ts | 2 +- packages/openapi3/src/openapi.ts | 101 +++---------- packages/openapi3/src/schema-emitter-3-0.ts | 2 +- packages/openapi3/src/schema-emitter-3-1.ts | 2 +- packages/openapi3/src/schema-emitter.ts | 9 +- packages/openapi3/src/types.ts | 5 +- .../src/{util.ts => utils/basic-util.ts} | 28 +--- .../openapi3/src/utils/component-key-util.ts | 139 ++++++++++++++++++ packages/openapi3/test/component.test.ts | 133 +++++++++++++++++ 10 files changed, 303 insertions(+), 120 deletions(-) rename packages/openapi3/src/{util.ts => utils/basic-util.ts} (82%) create mode 100644 packages/openapi3/src/utils/component-key-util.ts create mode 100644 packages/openapi3/test/component.test.ts diff --git a/packages/openapi3/src/examples.ts b/packages/openapi3/src/examples.ts index ffdc70e934..a9024fe1cd 100644 --- a/packages/openapi3/src/examples.ts +++ b/packages/openapi3/src/examples.ts @@ -17,7 +17,7 @@ import type { } from "@typespec/http"; import { getOpenAPI3StatusCodes } from "./status-codes.js"; import { OpenAPI3Example, OpenAPI3MediaType } from "./types.js"; -import { isSharedHttpOperation, SharedHttpOperation } from "./util.js"; +import { isSharedHttpOperation, SharedHttpOperation } from "./utils/basic-util.js"; export interface OperationExamples { requestBody: Record; diff --git a/packages/openapi3/src/openapi-helpers-3-1.ts b/packages/openapi3/src/openapi-helpers-3-1.ts index 18f2f58f00..9f9c524644 100644 --- a/packages/openapi3/src/openapi-helpers-3-1.ts +++ b/packages/openapi3/src/openapi-helpers-3-1.ts @@ -2,7 +2,7 @@ import { ModelProperty, Scalar } from "@typespec/compiler"; import { applyEncoding as baseApplyEncoding } from "./encoding.js"; import { OpenApiSpecSpecificProps } from "./openapi-spec-mappings.js"; import { OpenAPISchema3_1 } from "./types.js"; -import { isScalarExtendsBytes } from "./util.js"; +import { isScalarExtendsBytes } from "./utils/basic-util.js"; function getEncodingFieldName(typespecType: Scalar | ModelProperty) { // In Open API 3.1, `contentEncoding` is used for encoded binary data instead of `format`. diff --git a/packages/openapi3/src/openapi.ts b/packages/openapi3/src/openapi.ts index c88d6d3bfa..6e134ad83b 100644 --- a/packages/openapi3/src/openapi.ts +++ b/packages/openapi3/src/openapi.ts @@ -115,12 +115,16 @@ import { import { deepEquals, getDefaultValue, - invalidComponentFixedFieldKey, isBytesKeptRaw, isSharedHttpOperation, SharedHttpOperation, +} from "./utils/basic-util.js"; +import { + getComponentFixedFieldKeyContext, + setUpComponentFixedFieldKeyContext, + updateToValidRefs, validateComponentFixedFieldKey, -} from "./util.js"; +} from "./utils/component-key-util.js"; import { resolveVisibilityUsage, VisibilityUsageTracker } from "./visibility-usage.js"; import { resolveXmlModule, XmlModule } from "./xml-module.js"; @@ -274,9 +278,7 @@ function createOAPIEmitter( }, }; - program - .stateMap(invalidComponentFixedFieldKey) - .set(program.getGlobalNamespaceType(), new Map()); + const reportedDiagnostics = new Map(); return { emitOpenAPI, getOpenAPI }; @@ -645,6 +647,7 @@ function createOAPIEmitter( version?: string, ): Promise<[SupportedOpenAPIDocuments, Readonly] | undefined> { try { + setUpComponentFixedFieldKeyContext(program); const httpService = ignoreDiagnostics(getHttpService(program, service.type)); const auth = (serviceAuth = resolveAuthentication(httpService)); @@ -680,19 +683,15 @@ function createOAPIEmitter( } } - // Use valid keys in components/schemas and components/parameters - const invalidKeys = program - .stateMap(invalidComponentFixedFieldKey) - .get(program.getGlobalNamespaceType()) as Set; - useValidKeysInComponentFixedFields(invalidKeys, root.components?.schemas); - const invalidToValidKeyMap = useValidKeysInComponentFixedFields( - invalidKeys, - root.components?.parameters, - ); - for (const invalidKey in invalidToValidKeyMap) { - const validKey = invalidToValidKeyMap.get(invalidKey); - updateRefs(root, invalidKey, validKey!); + const diagnosticsForComponentKeys = getComponentFixedFieldKeyContext(program).diagnostics; + for (const [key, diagnostic] of diagnosticsForComponentKeys) { + if (reportedDiagnostics.has(key)) continue; + reportedDiagnostics.set(key, diagnostic); + program.reportDiagnostic(diagnostic); } + + updateToValidRefs(program, root); + return [root, diagnostics.diagnostics]; } catch (err) { if (err instanceof ErrorTypeFoundError) { @@ -703,72 +702,6 @@ function createOAPIEmitter( throw err; } } - - - // find anything uses references - - // TODO: OpenAPI3SchemaProperty - // TODO: OpenAPI3Link - - // TODO: OpenAPI3Responses.status - // TODO: OpenAPI3Response.headers - // TODO: OpenAPI3Response.links - // TODO: OpenAPI3MediaType.schema - // TODO: OpenAPI3Encoding.headers - // TODO: OpenAPI3Schema.allof/oneof/anyof/not/items/additionalProperties/ - // TODO: OpenAPI3Operation.requestBody - // TODO: OpenAPI3Operation.parameters - - // TODO: JsonSchema.contentSchema/allof/anyof/oneof/not/items/prefixItems/contains/properties/additionalProperties - // TODO:OpenAPIComponents3_1.responses/parameters/examples/requestBodies/headers/securitySchemes/links - function updateRefs(root: SupportedOpenAPIDocuments, oldModelName: string, newModelName: string) { - - } - - function useValidKeysInComponentFixedFields( - invalidKeys: Set, - pairs?: Record, - ): Map | undefined { - if (!pairs) return; - - const newPairs: typeof pairs = {}; - const originalKeys = Object.keys(pairs); - const validKeys = new Set(originalKeys.filter((key) => !invalidKeys.has(key))); - const invalidToValidKeyMap = new Map(); - - for (const [_, key] of originalKeys.entries()) { - if (validKeys.has(key)) { - newPairs[key] = pairs[key]; - continue; - } - - const newKey = createValidKey(key, validKeys, newPairs); - newPairs[newKey] = pairs[key]; - pairs = newPairs; - invalidToValidKeyMap.set(key, newKey); - } - - return invalidToValidKeyMap; - - function createValidKey( - invalidKey: string, - originalValidKeys: Set, - newPairs: Record, - ): string { - let baseKey = invalidKey.replace(/[^a-zA-Z0-9.\-_]/g, "_"); - let index = 1; - let validKey = baseKey; - - for ( - let validKey = baseKey; - validKey in originalValidKeys || validKey in newPairs; - index++ - ) { - validKey = baseKey + index; - } - return validKey; - } - } } function joinOps( @@ -1660,7 +1593,7 @@ function createOAPIEmitter( delete param[key]; } - param.$ref = "#/components/parameters/" + encodeURIComponent(key); + param.$ref = "#/components/parameters/" + key; } } diff --git a/packages/openapi3/src/schema-emitter-3-0.ts b/packages/openapi3/src/schema-emitter-3-0.ts index 3ddbd77477..0b6c69056e 100644 --- a/packages/openapi3/src/schema-emitter-3-0.ts +++ b/packages/openapi3/src/schema-emitter-3-0.ts @@ -31,7 +31,7 @@ import { CreateSchemaEmitter } from "./openapi-spec-mappings.js"; import { ResolvedOpenAPI3EmitterOptions } from "./openapi.js"; import { Builders, OpenAPI3SchemaEmitterBase } from "./schema-emitter.js"; import { JsonType, OpenAPI3Schema } from "./types.js"; -import { isBytesKeptRaw, isLiteralType, literalType } from "./util.js"; +import { isBytesKeptRaw, isLiteralType, literalType } from "./utils/basic-util.js"; import { VisibilityUsageTracker } from "./visibility-usage.js"; import { XmlModule } from "./xml-module.js"; diff --git a/packages/openapi3/src/schema-emitter-3-1.ts b/packages/openapi3/src/schema-emitter-3-1.ts index 6f1152377f..0664a6621d 100644 --- a/packages/openapi3/src/schema-emitter-3-1.ts +++ b/packages/openapi3/src/schema-emitter-3-1.ts @@ -34,7 +34,7 @@ import { CreateSchemaEmitter } from "./openapi-spec-mappings.js"; import { ResolvedOpenAPI3EmitterOptions } from "./openapi.js"; import { Builders, OpenAPI3SchemaEmitterBase } from "./schema-emitter.js"; import { JsonType, OpenAPISchema3_1 } from "./types.js"; -import { isBytesKeptRaw, isLiteralType, literalType } from "./util.js"; +import { isBytesKeptRaw, isLiteralType, literalType } from "./utils/basic-util.js"; import { VisibilityUsageTracker } from "./visibility-usage.js"; import { XmlModule } from "./xml-module.js"; diff --git a/packages/openapi3/src/schema-emitter.ts b/packages/openapi3/src/schema-emitter.ts index df36028ce4..539f03df6e 100644 --- a/packages/openapi3/src/schema-emitter.ts +++ b/packages/openapi3/src/schema-emitter.ts @@ -80,8 +80,8 @@ import { includeDerivedModel, isBytesKeptRaw, isStdType, - validateComponentFixedFieldKey, -} from "./util.js"; +} from "./utils/basic-util.js"; +import { validateComponentFixedFieldKey } from "./utils/component-key-util.js"; import { VisibilityUsageTracker } from "./visibility-usage.js"; import { XmlModule } from "./xml-module.js"; @@ -163,7 +163,6 @@ export class OpenAPI3SchemaEmitterBase< modelDeclaration(model: Model, _: string): EmitterOutput { const program = this.emitter.getProgram(); - validateComponentFixedFieldKey(program, model, model.name); const visibility = this.#getVisibilityContext(); const schema: ObjectBuilder = new ObjectBuilder({ type: "object", @@ -746,6 +745,10 @@ export class OpenAPI3SchemaEmitterBase< fullName, Object.fromEntries(decl.scope.declarations.map((x) => [x.name, true])), ); + + if (type.kind === "Model") { + validateComponentFixedFieldKey(this.emitter.getProgram(), type, fullName); + } return decl; } diff --git a/packages/openapi3/src/types.ts b/packages/openapi3/src/types.ts index fc1882bd02..9956814898 100644 --- a/packages/openapi3/src/types.ts +++ b/packages/openapi3/src/types.ts @@ -128,7 +128,8 @@ export interface OpenAPI3Tag extends Extensions { externalDocs?: OpenAPI3ExternalDocs; } -export type HttpMethod = "get" | "put" | "post" | "delete" | "options" | "head" | "patch" | "trace"; +export const httpMethods = ['get', 'put', 'post', 'delete', 'options', 'head', 'patch', 'trace'] as const; +export type HttpMethod = typeof httpMethods[number]; /** * Describes the operations available on a single path. A Path Item may be empty, due to ACL constraints. The path itself is still exposed to the documentation viewer but they will not know which operations and parameters are available. @@ -682,7 +683,7 @@ export type OpenAPI3Header = OpenAPI3ParameterBase & { export type OpenAPI3Operation = Extensions & { description?: string; summary?: string; - responses?: any; + responses?: any; tags?: string[]; operationId?: string; requestBody?: Refable; diff --git a/packages/openapi3/src/util.ts b/packages/openapi3/src/utils/basic-util.ts similarity index 82% rename from packages/openapi3/src/util.ts rename to packages/openapi3/src/utils/basic-util.ts index 6f4f2b3562..6690e0bbe2 100644 --- a/packages/openapi3/src/util.ts +++ b/packages/openapi3/src/utils/basic-util.ts @@ -14,7 +14,7 @@ import { Value, } from "@typespec/compiler"; import { HttpOperation } from "@typespec/http"; -import { createDiagnostic, createStateSymbol } from "./lib.js"; + /** * Checks if two objects are deeply equal. * @@ -159,29 +159,3 @@ export function getDefaultValue( export function isBytesKeptRaw(program: Program, type: Type) { return type.kind === "Scalar" && type.name === "bytes" && getEncode(program, type) === undefined; } - -export const invalidComponentFixedFieldKey = createStateSymbol("invalidComponentFixedField"); -export function validateComponentFixedFieldKey(program: Program, type: Type, name: string) { - if (!isValidComponentFixedFieldKey) { - const invalidKeys = program - .stateMap(invalidComponentFixedFieldKey) - .get(program.getGlobalNamespaceType()) as Set; - if (!invalidKeys.has(name)) { - invalidKeys.add(name); - } - program.reportDiagnostic( - createDiagnostic({ - code: "invalid-component-fixed-field-key", - format: { - value: name, - }, - target: type, - }), - ); - } -} - -export function isValidComponentFixedFieldKey(key: string) { - const validPattern = /^[a-zA-Z0-9.\-_]+$/; - return validPattern.test(key); -} diff --git a/packages/openapi3/src/utils/component-key-util.ts b/packages/openapi3/src/utils/component-key-util.ts new file mode 100644 index 0000000000..c095bc0eb8 --- /dev/null +++ b/packages/openapi3/src/utils/component-key-util.ts @@ -0,0 +1,139 @@ +import { Diagnostic, Program, Type } from "@typespec/compiler"; +import { createDiagnostic, createStateSymbol } from "../lib.js"; +import { SupportedOpenAPIDocuments } from "../types.js"; + +export interface ComponentFixedFieldKeyContext { + invalidKeys: Set; + diagnostics: Map; +} + +export const invalidComponentFixedFieldKey = createStateSymbol("invalidComponentFixedField"); + +export function setUpComponentFixedFieldKeyContext(program: Program) { + const context: ComponentFixedFieldKeyContext = { + invalidKeys: new Set(), + diagnostics: new Map(), + }; + program.stateMap(invalidComponentFixedFieldKey).set(program.getGlobalNamespaceType(), context); +} + +export function getComponentFixedFieldKeyContext(program: Program) { + return program + .stateMap(invalidComponentFixedFieldKey) + .get(program.getGlobalNamespaceType()) as ComponentFixedFieldKeyContext; +} + +export function validateComponentFixedFieldKey(program: Program, type: Type, keyInOpenAPI: string) { + if (!isValidComponentFixedFieldKey(keyInOpenAPI)) { + const { invalidKeys, diagnostics } = getComponentFixedFieldKeyContext(program); + if (!invalidKeys.has(keyInOpenAPI)) { + const isParameter = type.kind === "ModelProperty"; + if (isParameter) invalidKeys.add(keyInOpenAPI); + else invalidKeys.add(keyInOpenAPI); + } + const diagnostic = createDiagnostic({ + code: "invalid-component-fixed-field-key", + format: { + value: keyInOpenAPI, + }, + target: type, + }); + diagnostics.set(keyInOpenAPI, diagnostic); + } + + function isValidComponentFixedFieldKey(key: string) { + const validPattern = /^[a-zA-Z0-9.\-_]+$/; + return validPattern.test(key); + } +} + +export function useValidKeysInComponentFixedFields( + result: Map, + invalidKeys: Set, + pairs: Record, +): typeof pairs { + if (!pairs) return pairs; + + const newPairs: typeof pairs = {}; + const originalKeys = Object.keys(pairs); + const validKeys = new Set(originalKeys.filter((key) => !invalidKeys.has(key))); + + for (const [_, key] of originalKeys.entries()) { + if (validKeys.has(key)) { + newPairs[key] = pairs[key]; + continue; + } + + const newKey = createValidKey(key, validKeys, newPairs); + newPairs[newKey] = pairs[key]; + result.set(key, newKey); + } + + return newPairs; + + function createValidKey( + invalidKey: string, + originalValidKeys: Set, + newPairs: Record, + ): string { + let baseKey = invalidKey.replace(/[^a-zA-Z0-9.\-_]/g, "_"); + let index = 1; + let validKey = baseKey; + + for (let validKey = baseKey; validKey in originalValidKeys || validKey in newPairs; index++) { + validKey = baseKey + index; + } + return validKey; + } +} + +export function updateToValidRefs(program: Program, root: SupportedOpenAPIDocuments) { + const invalidRefs = getComponentFixedFieldKeyContext(program).invalidKeys; + const modelNameMap = new Map(); + if (root.components?.schemas) { + root.components.schemas = useValidKeysInComponentFixedFields( + modelNameMap, + invalidRefs, + root.components.schemas, + ) as typeof root.components.schemas; + } + if (root.components?.parameters) { + root.components.parameters = useValidKeysInComponentFixedFields( + modelNameMap, + invalidRefs, + root.components.parameters, + ) as typeof root.components.parameters; + } + for (const [invalidRef, validRef] of modelNameMap) { + updateRefs(root, invalidRef, validRef); + } + + function isParameterRef(ref: string) { + return ref.startsWith("#/components/parameters/"); + } + + function getParameterKey(ref: string) { + return ref.replace("#/components/parameters/", ""); + } + + function getSchemaKey(ref: string) { + return ref.replace("#/components/schemas/", ""); + } + + function updateRefs(obj: any, oldKey: string, newKey: string) { + if (obj.$ref) { + if (isParameterRef(obj.$ref)) { + const parameterKey = getParameterKey(obj.$ref); + if (parameterKey === oldKey) + obj.$ref = `#/components/parameters/${encodeURIComponent(newKey)}`; + } else { + const schemaKey = getSchemaKey(obj.$ref); + if (schemaKey === oldKey) obj.$ref = `#/components/schemas/${newKey}`; + } + } + for (const key in obj) { + if (key.startsWith("x-")) continue; + if (typeof obj[key] === "object") updateRefs(obj[key], oldKey, newKey); + } + } +} diff --git a/packages/openapi3/test/component.test.ts b/packages/openapi3/test/component.test.ts new file mode 100644 index 0000000000..887c6aaa75 --- /dev/null +++ b/packages/openapi3/test/component.test.ts @@ -0,0 +1,133 @@ +import { expectDiagnostics } from "@typespec/compiler/testing"; +import { describe, expect, it } from "vitest"; +import { OpenAPI3Document } from "../src/types.js"; +import { worksFor } from "./works-for.js"; + +interface Case { + title: string; + code: string; + invalidKey: string; + validKey: string; + typeName: string; + kind: Kind; +} + +enum Kind { + Model, + Parameter, +} + +const kindMap = { + [Kind.Model]: "Model", + [Kind.Parameter]: "ModelProperty", +}; + +worksFor(["3.0.0", "3.1.0"], async (specHelpers) => { + describe("Invalid component key", () => { + it.each([ + { + title: "Basic model case", + code: ` + @service + namespace Ns1Valid { + model \`foo-/inva*li\td\` {} + }`, + invalidKey: "foo-/inva*li\td", + validKey: "foo-_inva_li_d", + typeName: "foo-/inva*li\td", + kind: Kind.Model, + }, + { + title: "Nested model case", + code: ` + @service + namespace NsOut { + namespace NsNested { + model \`foo/invalid\` {} + } + }`, + invalidKey: "NsNested.foo/invalid", + validKey: "NsNested.foo_invalid", + typeName: "foo/invalid", + kind: Kind.Model, + }, + { + title: "Basic parameter case", + code: ` + @service + namespace Ns { + model Zoo { + @query + \`para/invalid\`: string; + b: string; + } + op get(...Zoo): string; + }`, + invalidKey: "Zoo.para/invalid", + validKey: "Zoo.para_invalid", + typeName: "para/invalid", + kind: Kind.Parameter, + }, + { + title: "Nested parameter case", + code: ` + @service + namespace Ns { + namespace NsNest { + model Zoo { + @query + \`para/invalid\`: string; + b: string; + } + op get(...Zoo): string; + } + }`, + invalidKey: "NsNest.Zoo.para/invalid", + validKey: "NsNest.Zoo.para_invalid", + typeName: "para/invalid", + kind: Kind.Parameter, + }, + ])("$title should report diagnostics and replace by valid key", async (c: Case) => { + const [doc, diag] = await specHelpers.emitOpenApiWithDiagnostics(c.code); + + // check diagnostics + expectDiagnostics(diag, [createExpectedDiagnostic(c.invalidKey)]); + const target = diag[0].target as any; + expect(target).toHaveProperty("kind"); + expect(target.kind).toBe(kindMap[c.kind]); + expect(target.name).toBe(c.typeName); + + // check generated doc + const componentField = getComponentField(doc, c.kind); + expect(componentField).toBeDefined(); + expect(componentField).not.toHaveProperty(c.invalidKey); + expect(componentField).toHaveProperty(c.validKey); + + // check ref: TODO + switch (c.kind) { + case Kind.Model: { + break; + } + case Kind.Parameter: { + break; + } + } + }); + }); +}); + +function getComponentField(doc: OpenAPI3Document, kind: Kind) { + switch (kind) { + case Kind.Model: + return doc.components?.schemas; + case Kind.Parameter: + return doc.components?.parameters; + } +} + +function createExpectedDiagnostic(key: string) { + return { + code: "@typespec/openapi3/invalid-component-fixed-field-key", + message: `Invalid key '${key}' used in a fixed field of the Component object. Only alphanumerics, dot (.), hyphen (-), and underscore (_) characters are allowed in keys.`, + }; +} From 501ec30b5d26a54d298007c9d71061f89378130a Mon Sep 17 00:00:00 2001 From: Wanpeng Li Date: Wed, 19 Feb 2025 11:16:57 +0800 Subject: [PATCH 09/25] add ref test --- packages/openapi3/test/component.test.ts | 25 +++++++++++++++++------- 1 file changed, 18 insertions(+), 7 deletions(-) diff --git a/packages/openapi3/test/component.test.ts b/packages/openapi3/test/component.test.ts index 887c6aaa75..cdd71f6ea0 100644 --- a/packages/openapi3/test/component.test.ts +++ b/packages/openapi3/test/component.test.ts @@ -22,6 +22,11 @@ const kindMap = { [Kind.Parameter]: "ModelProperty", }; +const prefixMap = { + [Kind.Model]: "#/components/schemas", + [Kind.Parameter]: "#/components/parameters", +}; + worksFor(["3.0.0", "3.1.0"], async (specHelpers) => { describe("Invalid component key", () => { it.each([ @@ -31,6 +36,7 @@ worksFor(["3.0.0", "3.1.0"], async (specHelpers) => { @service namespace Ns1Valid { model \`foo-/inva*li\td\` {} + op f(p: \`foo-/inva*li\td\`): void; }`, invalidKey: "foo-/inva*li\td", validKey: "foo-_inva_li_d", @@ -44,6 +50,7 @@ worksFor(["3.0.0", "3.1.0"], async (specHelpers) => { namespace NsOut { namespace NsNested { model \`foo/invalid\` {} + op f(p: \`foo/invalid\`): void; } }`, invalidKey: "NsNested.foo/invalid", @@ -61,7 +68,7 @@ worksFor(["3.0.0", "3.1.0"], async (specHelpers) => { \`para/invalid\`: string; b: string; } - op get(...Zoo): string; + op f(...Zoo): string; }`, invalidKey: "Zoo.para/invalid", validKey: "Zoo.para_invalid", @@ -79,7 +86,7 @@ worksFor(["3.0.0", "3.1.0"], async (specHelpers) => { \`para/invalid\`: string; b: string; } - op get(...Zoo): string; + op f(...Zoo): string; } }`, invalidKey: "NsNest.Zoo.para/invalid", @@ -103,14 +110,18 @@ worksFor(["3.0.0", "3.1.0"], async (specHelpers) => { expect(componentField).not.toHaveProperty(c.invalidKey); expect(componentField).toHaveProperty(c.validKey); - // check ref: TODO + // check ref switch (c.kind) { - case Kind.Model: { + case Kind.Model: + expect( + (doc as any).paths["/"].post.requestBody.content["application/json"].schema.properties.p.$ref, + ).toBe(`${prefixMap[c.kind]}/${c.validKey}`); break; - } - case Kind.Parameter: { + case Kind.Parameter: + expect((doc as any).paths["/"].post.parameters![0].$ref).toBe( + `${prefixMap[c.kind]}/${c.validKey}`, + ); break; - } } }); }); From 933694e71c05a0260e8546bcfbdc717ca755cf33 Mon Sep 17 00:00:00 2001 From: Wanpeng Li Date: Wed, 19 Feb 2025 11:27:41 +0800 Subject: [PATCH 10/25] . . --- packages/openapi3/src/types.ts | 15 ++++++++++++--- packages/openapi3/src/utils/basic-util.ts | 1 - 2 files changed, 12 insertions(+), 4 deletions(-) diff --git a/packages/openapi3/src/types.ts b/packages/openapi3/src/types.ts index 9956814898..9cc0392c50 100644 --- a/packages/openapi3/src/types.ts +++ b/packages/openapi3/src/types.ts @@ -128,8 +128,17 @@ export interface OpenAPI3Tag extends Extensions { externalDocs?: OpenAPI3ExternalDocs; } -export const httpMethods = ['get', 'put', 'post', 'delete', 'options', 'head', 'patch', 'trace'] as const; -export type HttpMethod = typeof httpMethods[number]; +export const httpMethods = [ + "get", + "put", + "post", + "delete", + "options", + "head", + "patch", + "trace", +] as const; +export type HttpMethod = (typeof httpMethods)[number]; /** * Describes the operations available on a single path. A Path Item may be empty, due to ACL constraints. The path itself is still exposed to the documentation viewer but they will not know which operations and parameters are available. @@ -683,7 +692,7 @@ export type OpenAPI3Header = OpenAPI3ParameterBase & { export type OpenAPI3Operation = Extensions & { description?: string; summary?: string; - responses?: any; + responses?: any; tags?: string[]; operationId?: string; requestBody?: Refable; diff --git a/packages/openapi3/src/utils/basic-util.ts b/packages/openapi3/src/utils/basic-util.ts index 6690e0bbe2..929be66094 100644 --- a/packages/openapi3/src/utils/basic-util.ts +++ b/packages/openapi3/src/utils/basic-util.ts @@ -14,7 +14,6 @@ import { Value, } from "@typespec/compiler"; import { HttpOperation } from "@typespec/http"; - /** * Checks if two objects are deeply equal. * From 7824fc3637df68419ec5167a0e1fffd093706009 Mon Sep 17 00:00:00 2001 From: Wanpeng Li Date: Wed, 19 Feb 2025 17:40:58 +0800 Subject: [PATCH 11/25] revert --- packages/openapi3/src/types.ts | 12 +----------- 1 file changed, 1 insertion(+), 11 deletions(-) diff --git a/packages/openapi3/src/types.ts b/packages/openapi3/src/types.ts index 9cc0392c50..fc1882bd02 100644 --- a/packages/openapi3/src/types.ts +++ b/packages/openapi3/src/types.ts @@ -128,17 +128,7 @@ export interface OpenAPI3Tag extends Extensions { externalDocs?: OpenAPI3ExternalDocs; } -export const httpMethods = [ - "get", - "put", - "post", - "delete", - "options", - "head", - "patch", - "trace", -] as const; -export type HttpMethod = (typeof httpMethods)[number]; +export type HttpMethod = "get" | "put" | "post" | "delete" | "options" | "head" | "patch" | "trace"; /** * Describes the operations available on a single path. A Path Item may be empty, due to ACL constraints. The path itself is still exposed to the documentation viewer but they will not know which operations and parameters are available. From 04a1de2570361321579f4f6e9da4bb7e9a962e3c Mon Sep 17 00:00:00 2001 From: albertxavier100 Date: Mon, 24 Feb 2025 13:45:12 +0800 Subject: [PATCH 12/25] update --- packages/openapi3/src/examples.ts | 2 +- packages/openapi3/src/openapi-helpers-3-1.ts | 2 +- packages/openapi3/src/openapi.ts | 42 ++- packages/openapi3/src/schema-emitter-3-0.ts | 2 +- packages/openapi3/src/schema-emitter-3-1.ts | 2 +- packages/openapi3/src/schema-emitter.ts | 56 +++- .../src/{utils/basic-util.ts => util.ts} | 41 +++ .../openapi3/src/utils/component-key-util.ts | 139 ---------- packages/openapi3/test/component.test.ts | 239 ++++++++++-------- 9 files changed, 243 insertions(+), 282 deletions(-) rename packages/openapi3/src/{utils/basic-util.ts => util.ts} (76%) delete mode 100644 packages/openapi3/src/utils/component-key-util.ts diff --git a/packages/openapi3/src/examples.ts b/packages/openapi3/src/examples.ts index a9024fe1cd..ffdc70e934 100644 --- a/packages/openapi3/src/examples.ts +++ b/packages/openapi3/src/examples.ts @@ -17,7 +17,7 @@ import type { } from "@typespec/http"; import { getOpenAPI3StatusCodes } from "./status-codes.js"; import { OpenAPI3Example, OpenAPI3MediaType } from "./types.js"; -import { isSharedHttpOperation, SharedHttpOperation } from "./utils/basic-util.js"; +import { isSharedHttpOperation, SharedHttpOperation } from "./util.js"; export interface OperationExamples { requestBody: Record; diff --git a/packages/openapi3/src/openapi-helpers-3-1.ts b/packages/openapi3/src/openapi-helpers-3-1.ts index 9f9c524644..18f2f58f00 100644 --- a/packages/openapi3/src/openapi-helpers-3-1.ts +++ b/packages/openapi3/src/openapi-helpers-3-1.ts @@ -2,7 +2,7 @@ import { ModelProperty, Scalar } from "@typespec/compiler"; import { applyEncoding as baseApplyEncoding } from "./encoding.js"; import { OpenApiSpecSpecificProps } from "./openapi-spec-mappings.js"; import { OpenAPISchema3_1 } from "./types.js"; -import { isScalarExtendsBytes } from "./utils/basic-util.js"; +import { isScalarExtendsBytes } from "./util.js"; function getEncodingFieldName(typespecType: Scalar | ModelProperty) { // In Open API 3.1, `contentEncoding` is used for encoded binary data instead of `format`. diff --git a/packages/openapi3/src/openapi.ts b/packages/openapi3/src/openapi.ts index 6e134ad83b..16881f0de5 100644 --- a/packages/openapi3/src/openapi.ts +++ b/packages/openapi3/src/openapi.ts @@ -114,17 +114,12 @@ import { } from "./types.js"; import { deepEquals, + ensureValidComponentFixedFieldKey, getDefaultValue, isBytesKeptRaw, isSharedHttpOperation, SharedHttpOperation, -} from "./utils/basic-util.js"; -import { - getComponentFixedFieldKeyContext, - setUpComponentFixedFieldKeyContext, - updateToValidRefs, - validateComponentFixedFieldKey, -} from "./utils/component-key-util.js"; +} from "./util.js"; import { resolveVisibilityUsage, VisibilityUsageTracker } from "./visibility-usage.js"; import { resolveXmlModule, XmlModule } from "./xml-module.js"; @@ -278,8 +273,6 @@ function createOAPIEmitter( }, }; - const reportedDiagnostics = new Map(); - return { emitOpenAPI, getOpenAPI }; async function emitOpenAPI() { @@ -647,7 +640,6 @@ function createOAPIEmitter( version?: string, ): Promise<[SupportedOpenAPIDocuments, Readonly] | undefined> { try { - setUpComponentFixedFieldKeyContext(program); const httpService = ignoreDiagnostics(getHttpService(program, service.type)); const auth = (serviceAuth = resolveAuthentication(httpService)); @@ -682,16 +674,6 @@ function createOAPIEmitter( } } } - - const diagnosticsForComponentKeys = getComponentFixedFieldKeyContext(program).diagnostics; - for (const [key, diagnostic] of diagnosticsForComponentKeys) { - if (reportedDiagnostics.has(key)) continue; - reportedDiagnostics.set(key, diagnostic); - program.reportDiagnostic(diagnostic); - } - - updateToValidRefs(program, root); - return [root, diagnostics.diagnostics]; } catch (err) { if (err instanceof ErrorTypeFoundError) { @@ -1587,13 +1569,23 @@ function createOAPIEmitter( root.components!.parameters!, typeNameOptions, ); - validateComponentFixedFieldKey(program, property, key); root.components!.parameters![key] = { ...param }; - for (const key of Object.keys(param)) { - delete param[key]; - } - param.$ref = "#/components/parameters/" + key; + ensureValidComponentFixedFieldKey( + program, + property, + () => undefined, + (_) => {}, + () => key, + (newKey) => { + root.components!.parameters![newKey] = { ...param }; + delete root.components?.parameters![key]; + for (const key of Object.keys(param)) { + delete param[key]; + } + param.$ref = "#/components/parameters/" + newKey; + }, + ); } } diff --git a/packages/openapi3/src/schema-emitter-3-0.ts b/packages/openapi3/src/schema-emitter-3-0.ts index 0b6c69056e..3ddbd77477 100644 --- a/packages/openapi3/src/schema-emitter-3-0.ts +++ b/packages/openapi3/src/schema-emitter-3-0.ts @@ -31,7 +31,7 @@ import { CreateSchemaEmitter } from "./openapi-spec-mappings.js"; import { ResolvedOpenAPI3EmitterOptions } from "./openapi.js"; import { Builders, OpenAPI3SchemaEmitterBase } from "./schema-emitter.js"; import { JsonType, OpenAPI3Schema } from "./types.js"; -import { isBytesKeptRaw, isLiteralType, literalType } from "./utils/basic-util.js"; +import { isBytesKeptRaw, isLiteralType, literalType } from "./util.js"; import { VisibilityUsageTracker } from "./visibility-usage.js"; import { XmlModule } from "./xml-module.js"; diff --git a/packages/openapi3/src/schema-emitter-3-1.ts b/packages/openapi3/src/schema-emitter-3-1.ts index 0664a6621d..6f1152377f 100644 --- a/packages/openapi3/src/schema-emitter-3-1.ts +++ b/packages/openapi3/src/schema-emitter-3-1.ts @@ -34,7 +34,7 @@ import { CreateSchemaEmitter } from "./openapi-spec-mappings.js"; import { ResolvedOpenAPI3EmitterOptions } from "./openapi.js"; import { Builders, OpenAPI3SchemaEmitterBase } from "./schema-emitter.js"; import { JsonType, OpenAPISchema3_1 } from "./types.js"; -import { isBytesKeptRaw, isLiteralType, literalType } from "./utils/basic-util.js"; +import { isBytesKeptRaw, isLiteralType, literalType } from "./util.js"; import { VisibilityUsageTracker } from "./visibility-usage.js"; import { XmlModule } from "./xml-module.js"; diff --git a/packages/openapi3/src/schema-emitter.ts b/packages/openapi3/src/schema-emitter.ts index 539f03df6e..e733262b50 100644 --- a/packages/openapi3/src/schema-emitter.ts +++ b/packages/openapi3/src/schema-emitter.ts @@ -76,12 +76,12 @@ import { OpenAPISchema3_1, } from "./types.js"; import { + ensureValidComponentFixedFieldKey, getDefaultValue, includeDerivedModel, isBytesKeptRaw, isStdType, -} from "./utils/basic-util.js"; -import { validateComponentFixedFieldKey } from "./utils/component-key-util.js"; +} from "./util.js"; import { VisibilityUsageTracker } from "./visibility-usage.js"; import { XmlModule } from "./xml-module.js"; @@ -189,7 +189,17 @@ export class OpenAPI3SchemaEmitterBase< const baseName = getOpenAPITypeName(program, model, this.#typeNameOptions()); const isMultipart = this.getContentType().startsWith("multipart/"); - const name = isMultipart ? baseName + "MultiPart" : baseName; + let name = isMultipart ? baseName + "MultiPart" : baseName; + + ensureValidComponentFixedFieldKey( + program, + model, + () => model.name, + (newKey) => (model.name = newKey), + () => name, + (newKey) => (name = newKey), + ); + return this.#createDeclaration(model, name, this.applyConstraints(model, schema as any)); } @@ -462,7 +472,17 @@ export class OpenAPI3SchemaEmitterBase< } enumDeclaration(en: Enum, name: string): EmitterOutput { - const baseName = getOpenAPITypeName(this.emitter.getProgram(), en, this.#typeNameOptions()); + let baseName = getOpenAPITypeName(this.emitter.getProgram(), en, this.#typeNameOptions()); + + ensureValidComponentFixedFieldKey( + this.emitter.getProgram(), + en, + () => en.name, + (newKey) => (en.name = newKey), + () => baseName, + (newKey) => (baseName = newKey), + ); + return this.#createDeclaration(en, baseName, new ObjectBuilder(this.enumSchema(en))); } @@ -489,7 +509,19 @@ export class OpenAPI3SchemaEmitterBase< unionDeclaration(union: Union, name: string): EmitterOutput { const schema = this.unionSchema(union); - const baseName = getOpenAPITypeName(this.emitter.getProgram(), union, this.#typeNameOptions()); + let baseName = getOpenAPITypeName(this.emitter.getProgram(), union, this.#typeNameOptions()); + + ensureValidComponentFixedFieldKey( + this.emitter.getProgram(), + union, + () => union.name, + (newKey) => { + if (union.name) union.name = newKey; + }, + () => baseName, + (newKey) => (baseName = newKey), + ); + return this.#createDeclaration(union, baseName, schema); } @@ -572,7 +604,16 @@ export class OpenAPI3SchemaEmitterBase< scalarDeclaration(scalar: Scalar, name: string): EmitterOutput { const isStd = isStdType(this.emitter.getProgram(), scalar); const schema = this.#getSchemaForScalar(scalar); - const baseName = getOpenAPITypeName(this.emitter.getProgram(), scalar, this.#typeNameOptions()); + let baseName = getOpenAPITypeName(this.emitter.getProgram(), scalar, this.#typeNameOptions()); + + ensureValidComponentFixedFieldKey( + this.emitter.getProgram(), + scalar, + () => scalar.name, + (newKey) => (scalar.name = newKey), + () => baseName, + (newKey) => (baseName = newKey), + ); // Don't create a declaration for std types return isStd @@ -746,9 +787,6 @@ export class OpenAPI3SchemaEmitterBase< Object.fromEntries(decl.scope.declarations.map((x) => [x.name, true])), ); - if (type.kind === "Model") { - validateComponentFixedFieldKey(this.emitter.getProgram(), type, fullName); - } return decl; } diff --git a/packages/openapi3/src/utils/basic-util.ts b/packages/openapi3/src/util.ts similarity index 76% rename from packages/openapi3/src/utils/basic-util.ts rename to packages/openapi3/src/util.ts index 929be66094..c081f8a854 100644 --- a/packages/openapi3/src/utils/basic-util.ts +++ b/packages/openapi3/src/util.ts @@ -14,6 +14,7 @@ import { Value, } from "@typespec/compiler"; import { HttpOperation } from "@typespec/http"; +import { createDiagnostic } from "./lib.js"; /** * Checks if two objects are deeply equal. * @@ -158,3 +159,43 @@ export function getDefaultValue( export function isBytesKeptRaw(program: Program, type: Type) { return type.kind === "Scalar" && type.name === "bytes" && getEncode(program, type) === undefined; } + +export function ensureValidComponentFixedFieldKey( + program: Program, + type: Type, + getEntityKey: () => string | undefined, + setEntityKey: (newKey: string) => void, + getDeclarationKey: () => string, + setDeclarationKey: (newKey: string) => void, +): void { + const oldDeclarationKey = getDeclarationKey(); + if (isValidComponentFixedFieldKey(oldDeclarationKey)) return; + reportInvalidKey(program, type, oldDeclarationKey); + const newDeclarationKey = createValidKey(oldDeclarationKey); + setDeclarationKey(newDeclarationKey); + const oldEntityKey = getEntityKey(); + if (oldEntityKey) { + const newEntityKey = createValidKey(oldEntityKey); + setEntityKey(newEntityKey); + } +} + +function isValidComponentFixedFieldKey(key: string) { + const validPattern = /^[a-zA-Z0-9.\-_]+$/; + return validPattern.test(key); +} + +function reportInvalidKey(program: Program, type: Type, key: string) { + const diagnostic = createDiagnostic({ + code: "invalid-component-fixed-field-key", + format: { + value: key, + }, + target: type, + }); + return program.reportDiagnostic(diagnostic); +} + +function createValidKey(invalidKey: string): string { + return invalidKey.replace(/[^a-zA-Z0-9.\-_]/g, "_"); +} diff --git a/packages/openapi3/src/utils/component-key-util.ts b/packages/openapi3/src/utils/component-key-util.ts deleted file mode 100644 index c095bc0eb8..0000000000 --- a/packages/openapi3/src/utils/component-key-util.ts +++ /dev/null @@ -1,139 +0,0 @@ -import { Diagnostic, Program, Type } from "@typespec/compiler"; -import { createDiagnostic, createStateSymbol } from "../lib.js"; -import { SupportedOpenAPIDocuments } from "../types.js"; - -export interface ComponentFixedFieldKeyContext { - invalidKeys: Set; - diagnostics: Map; -} - -export const invalidComponentFixedFieldKey = createStateSymbol("invalidComponentFixedField"); - -export function setUpComponentFixedFieldKeyContext(program: Program) { - const context: ComponentFixedFieldKeyContext = { - invalidKeys: new Set(), - diagnostics: new Map(), - }; - program.stateMap(invalidComponentFixedFieldKey).set(program.getGlobalNamespaceType(), context); -} - -export function getComponentFixedFieldKeyContext(program: Program) { - return program - .stateMap(invalidComponentFixedFieldKey) - .get(program.getGlobalNamespaceType()) as ComponentFixedFieldKeyContext; -} - -export function validateComponentFixedFieldKey(program: Program, type: Type, keyInOpenAPI: string) { - if (!isValidComponentFixedFieldKey(keyInOpenAPI)) { - const { invalidKeys, diagnostics } = getComponentFixedFieldKeyContext(program); - if (!invalidKeys.has(keyInOpenAPI)) { - const isParameter = type.kind === "ModelProperty"; - if (isParameter) invalidKeys.add(keyInOpenAPI); - else invalidKeys.add(keyInOpenAPI); - } - const diagnostic = createDiagnostic({ - code: "invalid-component-fixed-field-key", - format: { - value: keyInOpenAPI, - }, - target: type, - }); - diagnostics.set(keyInOpenAPI, diagnostic); - } - - function isValidComponentFixedFieldKey(key: string) { - const validPattern = /^[a-zA-Z0-9.\-_]+$/; - return validPattern.test(key); - } -} - -export function useValidKeysInComponentFixedFields( - result: Map, - invalidKeys: Set, - pairs: Record, -): typeof pairs { - if (!pairs) return pairs; - - const newPairs: typeof pairs = {}; - const originalKeys = Object.keys(pairs); - const validKeys = new Set(originalKeys.filter((key) => !invalidKeys.has(key))); - - for (const [_, key] of originalKeys.entries()) { - if (validKeys.has(key)) { - newPairs[key] = pairs[key]; - continue; - } - - const newKey = createValidKey(key, validKeys, newPairs); - newPairs[newKey] = pairs[key]; - result.set(key, newKey); - } - - return newPairs; - - function createValidKey( - invalidKey: string, - originalValidKeys: Set, - newPairs: Record, - ): string { - let baseKey = invalidKey.replace(/[^a-zA-Z0-9.\-_]/g, "_"); - let index = 1; - let validKey = baseKey; - - for (let validKey = baseKey; validKey in originalValidKeys || validKey in newPairs; index++) { - validKey = baseKey + index; - } - return validKey; - } -} - -export function updateToValidRefs(program: Program, root: SupportedOpenAPIDocuments) { - const invalidRefs = getComponentFixedFieldKeyContext(program).invalidKeys; - const modelNameMap = new Map(); - if (root.components?.schemas) { - root.components.schemas = useValidKeysInComponentFixedFields( - modelNameMap, - invalidRefs, - root.components.schemas, - ) as typeof root.components.schemas; - } - if (root.components?.parameters) { - root.components.parameters = useValidKeysInComponentFixedFields( - modelNameMap, - invalidRefs, - root.components.parameters, - ) as typeof root.components.parameters; - } - for (const [invalidRef, validRef] of modelNameMap) { - updateRefs(root, invalidRef, validRef); - } - - function isParameterRef(ref: string) { - return ref.startsWith("#/components/parameters/"); - } - - function getParameterKey(ref: string) { - return ref.replace("#/components/parameters/", ""); - } - - function getSchemaKey(ref: string) { - return ref.replace("#/components/schemas/", ""); - } - - function updateRefs(obj: any, oldKey: string, newKey: string) { - if (obj.$ref) { - if (isParameterRef(obj.$ref)) { - const parameterKey = getParameterKey(obj.$ref); - if (parameterKey === oldKey) - obj.$ref = `#/components/parameters/${encodeURIComponent(newKey)}`; - } else { - const schemaKey = getSchemaKey(obj.$ref); - if (schemaKey === oldKey) obj.$ref = `#/components/schemas/${newKey}`; - } - } - for (const key in obj) { - if (key.startsWith("x-")) continue; - if (typeof obj[key] === "object") updateRefs(obj[key], oldKey, newKey); - } - } -} diff --git a/packages/openapi3/test/component.test.ts b/packages/openapi3/test/component.test.ts index cdd71f6ea0..d7556375b1 100644 --- a/packages/openapi3/test/component.test.ts +++ b/packages/openapi3/test/component.test.ts @@ -3,135 +3,164 @@ import { describe, expect, it } from "vitest"; import { OpenAPI3Document } from "../src/types.js"; import { worksFor } from "./works-for.js"; -interface Case { - title: string; - code: string; +interface DiagnosticCheck { invalidKey: string; - validKey: string; - typeName: string; - kind: Kind; + declarationKey: string; + prefix: "#/components/schemas/" | "#/components/parameters/"; } -enum Kind { - Model, - Parameter, +interface Case { + title: string; + code: string; + diagChecks: DiagnosticCheck[]; + refChecks?: (doc: any) => void; } -const kindMap = { - [Kind.Model]: "Model", - [Kind.Parameter]: "ModelProperty", +const prefixToKindMap = { + ["#/components/schemas/"]: "Model", + ["#/components/parameters/"]: "ModelProperty", }; -const prefixMap = { - [Kind.Model]: "#/components/schemas", - [Kind.Parameter]: "#/components/parameters", -}; +const testCases: Case[] = [ + { + title: "Basic model case", + code: ` + @service + namespace Ns1Valid { + model \`foo-/inva*li\td\` {} + op f(p: \`foo-/inva*li\td\`): void; + }`, -worksFor(["3.0.0", "3.1.0"], async (specHelpers) => { - describe("Invalid component key", () => { - it.each([ + diagChecks: [ { - title: "Basic model case", - code: ` - @service - namespace Ns1Valid { - model \`foo-/inva*li\td\` {} - op f(p: \`foo-/inva*li\td\`): void; - }`, invalidKey: "foo-/inva*li\td", - validKey: "foo-_inva_li_d", - typeName: "foo-/inva*li\td", - kind: Kind.Model, + declarationKey: "foo-_inva_li_d", + prefix: "#/components/schemas/", }, + ], + refChecks: (doc) => { + expect( + doc.paths["/"].post.requestBody.content["application/json"].schema.properties.p.$ref, + ).toBe("#/components/schemas/foo-_inva_li_d"); + }, + }, + { + title: "Basic parameter case", + code: ` + @service + namespace Ns { + model Zoo { + @query + \`para/invalid\`: string; + b: string; + } + op f(...Zoo): string; + }`, + diagChecks: [ { - title: "Nested model case", - code: ` - @service - namespace NsOut { - namespace NsNested { - model \`foo/invalid\` {} - op f(p: \`foo/invalid\`): void; - } - }`, - invalidKey: "NsNested.foo/invalid", - validKey: "NsNested.foo_invalid", - typeName: "foo/invalid", - kind: Kind.Model, + invalidKey: "Zoo.para/invalid", + declarationKey: "Zoo.para_invalid", + prefix: "#/components/parameters/", }, + ], + refChecks: (doc) => { + expect(doc.paths["/"].post.parameters[0].$ref).toBe( + "#/components/parameters/Zoo.para_invalid", + ); + }, + }, + { + title: "Nested model case", + code: ` + @service + namespace Ns1Valid { + model \`Nested/Model\` { + a: string; + } + model MMM { + b: \`Nested/Model\`; + } + op f(p: MMM): void; + }`, + diagChecks: [ { - title: "Basic parameter case", - code: ` - @service - namespace Ns { - model Zoo { - @query - \`para/invalid\`: string; - b: string; - } - op f(...Zoo): string; - }`, - invalidKey: "Zoo.para/invalid", - validKey: "Zoo.para_invalid", - typeName: "para/invalid", - kind: Kind.Parameter, + invalidKey: "Nested/Model", + declarationKey: "Nested_Model", + prefix: "#/components/schemas/", }, + ], + }, + { + title: "Nested parameter case", + code: ` + @service + namespace NS { + model \`Nested/Model\` { + a: string; + } + model MMM { + @query \`b/b\`: \`Nested/Model\`; + d: string; + } + op f(...MMM): void; + }`, + diagChecks: [ { - title: "Nested parameter case", - code: ` - @service - namespace Ns { - namespace NsNest { - model Zoo { - @query - \`para/invalid\`: string; - b: string; - } - op f(...Zoo): string; - } - }`, - invalidKey: "NsNest.Zoo.para/invalid", - validKey: "NsNest.Zoo.para_invalid", - typeName: "para/invalid", - kind: Kind.Parameter, + invalidKey: "Nested/Model", + declarationKey: "Nested_Model", + prefix: "#/components/schemas/", }, - ])("$title should report diagnostics and replace by valid key", async (c: Case) => { - const [doc, diag] = await specHelpers.emitOpenApiWithDiagnostics(c.code); - - // check diagnostics - expectDiagnostics(diag, [createExpectedDiagnostic(c.invalidKey)]); - const target = diag[0].target as any; - expect(target).toHaveProperty("kind"); - expect(target.kind).toBe(kindMap[c.kind]); - expect(target.name).toBe(c.typeName); + { + invalidKey: "MMM.b/b", + declarationKey: "MMM.b_b", + prefix: "#/components/parameters/", + }, + ], + refChecks: (doc) => { + expect(doc.components.parameters["MMM.b_b"].schema.$ref).toBe( + "#/components/schemas/Nested_Model", + ); + expect(doc.paths["/"].post.parameters[0].$ref).toBe("#/components/parameters/MMM.b_b"); + }, + }, +]; - // check generated doc - const componentField = getComponentField(doc, c.kind); - expect(componentField).toBeDefined(); - expect(componentField).not.toHaveProperty(c.invalidKey); - expect(componentField).toHaveProperty(c.validKey); +worksFor(["3.0.0", "3.1.0"], async (specHelpers) => { + describe("Invalid component key", () => { + it.each(testCases)( + "$title should report diagnostics and replace by valid key", + async (c: Case) => { + const [doc, diag] = await specHelpers.emitOpenApiWithDiagnostics(c.code); - // check ref - switch (c.kind) { - case Kind.Model: - expect( - (doc as any).paths["/"].post.requestBody.content["application/json"].schema.properties.p.$ref, - ).toBe(`${prefixMap[c.kind]}/${c.validKey}`); - break; - case Kind.Parameter: - expect((doc as any).paths["/"].post.parameters![0].$ref).toBe( - `${prefixMap[c.kind]}/${c.validKey}`, - ); - break; - } - }); + // check diagnostics + expectDiagnostics( + diag, + c.diagChecks.map((d) => createExpectedDiagnostic(d.invalidKey)), + ); + for (const [i, d] of c.diagChecks.entries()) { + const target = diag[i].target as any; + expect(target).toHaveProperty("kind"); + expect(target.kind).toBe(prefixToKindMap[d.prefix]); + // check generated doc + const componentField = getComponentField(doc, d.prefix); + expect(componentField).toBeDefined(); + expect(componentField).toHaveProperty(d.declarationKey); + } + // check ref + if (c.refChecks) c.refChecks(doc); + }, + ); }); }); -function getComponentField(doc: OpenAPI3Document, kind: Kind) { +function getComponentField( + doc: OpenAPI3Document, + kind: "#/components/schemas/" | "#/components/parameters/", +) { switch (kind) { - case Kind.Model: + case "#/components/schemas/": return doc.components?.schemas; - case Kind.Parameter: + case "#/components/parameters/": return doc.components?.parameters; } } From ddb29456724952255b0c096ccd3c0296ea4430f3 Mon Sep 17 00:00:00 2001 From: albertxavier100 Date: Mon, 24 Feb 2025 13:47:49 +0800 Subject: [PATCH 13/25] rename --- packages/openapi3/test/component.test.ts | 44 ++++++++++++------------ 1 file changed, 22 insertions(+), 22 deletions(-) diff --git a/packages/openapi3/test/component.test.ts b/packages/openapi3/test/component.test.ts index d7556375b1..3ad4c31555 100644 --- a/packages/openapi3/test/component.test.ts +++ b/packages/openapi3/test/component.test.ts @@ -4,9 +4,9 @@ import { OpenAPI3Document } from "../src/types.js"; import { worksFor } from "./works-for.js"; interface DiagnosticCheck { - invalidKey: string; - declarationKey: string; - prefix: "#/components/schemas/" | "#/components/parameters/"; + expectedDiagInvalidKey: string; + expectedDeclKey: string; + expectedPrefix: "#/components/schemas/" | "#/components/parameters/"; } interface Case { @@ -33,9 +33,9 @@ const testCases: Case[] = [ diagChecks: [ { - invalidKey: "foo-/inva*li\td", - declarationKey: "foo-_inva_li_d", - prefix: "#/components/schemas/", + expectedDiagInvalidKey: "foo-/inva*li\td", + expectedDeclKey: "foo-_inva_li_d", + expectedPrefix: "#/components/schemas/", }, ], refChecks: (doc) => { @@ -58,9 +58,9 @@ const testCases: Case[] = [ }`, diagChecks: [ { - invalidKey: "Zoo.para/invalid", - declarationKey: "Zoo.para_invalid", - prefix: "#/components/parameters/", + expectedDiagInvalidKey: "Zoo.para/invalid", + expectedDeclKey: "Zoo.para_invalid", + expectedPrefix: "#/components/parameters/", }, ], refChecks: (doc) => { @@ -84,9 +84,9 @@ const testCases: Case[] = [ }`, diagChecks: [ { - invalidKey: "Nested/Model", - declarationKey: "Nested_Model", - prefix: "#/components/schemas/", + expectedDiagInvalidKey: "Nested/Model", + expectedDeclKey: "Nested_Model", + expectedPrefix: "#/components/schemas/", }, ], }, @@ -106,14 +106,14 @@ const testCases: Case[] = [ }`, diagChecks: [ { - invalidKey: "Nested/Model", - declarationKey: "Nested_Model", - prefix: "#/components/schemas/", + expectedDiagInvalidKey: "Nested/Model", + expectedDeclKey: "Nested_Model", + expectedPrefix: "#/components/schemas/", }, { - invalidKey: "MMM.b/b", - declarationKey: "MMM.b_b", - prefix: "#/components/parameters/", + expectedDiagInvalidKey: "MMM.b/b", + expectedDeclKey: "MMM.b_b", + expectedPrefix: "#/components/parameters/", }, ], refChecks: (doc) => { @@ -135,16 +135,16 @@ worksFor(["3.0.0", "3.1.0"], async (specHelpers) => { // check diagnostics expectDiagnostics( diag, - c.diagChecks.map((d) => createExpectedDiagnostic(d.invalidKey)), + c.diagChecks.map((d) => createExpectedDiagnostic(d.expectedDiagInvalidKey)), ); for (const [i, d] of c.diagChecks.entries()) { const target = diag[i].target as any; expect(target).toHaveProperty("kind"); - expect(target.kind).toBe(prefixToKindMap[d.prefix]); + expect(target.kind).toBe(prefixToKindMap[d.expectedPrefix]); // check generated doc - const componentField = getComponentField(doc, d.prefix); + const componentField = getComponentField(doc, d.expectedPrefix); expect(componentField).toBeDefined(); - expect(componentField).toHaveProperty(d.declarationKey); + expect(componentField).toHaveProperty(d.expectedDeclKey); } // check ref if (c.refChecks) c.refChecks(doc); From 90ce764463727f9ddf5dd60eec13fe330ba1da42 Mon Sep 17 00:00:00 2001 From: albertxavier100 Date: Mon, 24 Feb 2025 14:04:12 +0800 Subject: [PATCH 14/25] more tests --- packages/openapi3/test/component.test.ts | 99 ++++++++++++++++++++++-- 1 file changed, 93 insertions(+), 6 deletions(-) diff --git a/packages/openapi3/test/component.test.ts b/packages/openapi3/test/component.test.ts index 3ad4c31555..ab0d78bf86 100644 --- a/packages/openapi3/test/component.test.ts +++ b/packages/openapi3/test/component.test.ts @@ -7,6 +7,7 @@ interface DiagnosticCheck { expectedDiagInvalidKey: string; expectedDeclKey: string; expectedPrefix: "#/components/schemas/" | "#/components/parameters/"; + kind: "Model" | "ModelProperty" | "Union"; } interface Case { @@ -16,11 +17,6 @@ interface Case { refChecks?: (doc: any) => void; } -const prefixToKindMap = { - ["#/components/schemas/"]: "Model", - ["#/components/parameters/"]: "ModelProperty", -}; - const testCases: Case[] = [ { title: "Basic model case", @@ -36,6 +32,7 @@ const testCases: Case[] = [ expectedDiagInvalidKey: "foo-/inva*li\td", expectedDeclKey: "foo-_inva_li_d", expectedPrefix: "#/components/schemas/", + kind: "Model", }, ], refChecks: (doc) => { @@ -61,6 +58,7 @@ const testCases: Case[] = [ expectedDiagInvalidKey: "Zoo.para/invalid", expectedDeclKey: "Zoo.para_invalid", expectedPrefix: "#/components/parameters/", + kind: "ModelProperty", }, ], refChecks: (doc) => { @@ -87,6 +85,7 @@ const testCases: Case[] = [ expectedDiagInvalidKey: "Nested/Model", expectedDeclKey: "Nested_Model", expectedPrefix: "#/components/schemas/", + kind: "Model", }, ], }, @@ -109,11 +108,13 @@ const testCases: Case[] = [ expectedDiagInvalidKey: "Nested/Model", expectedDeclKey: "Nested_Model", expectedPrefix: "#/components/schemas/", + kind: "Model", }, { expectedDiagInvalidKey: "MMM.b/b", expectedDeclKey: "MMM.b_b", expectedPrefix: "#/components/parameters/", + kind: "ModelProperty", }, ], refChecks: (doc) => { @@ -123,6 +124,92 @@ const testCases: Case[] = [ expect(doc.paths["/"].post.parameters[0].$ref).toBe("#/components/parameters/MMM.b_b"); }, }, + { + title: "Basic discriminator case", + code: ` + @service + namespace NS { + @discriminator("kind") + union \`Pe/t\` { + cat: \`C/at\`, + dog: \`Do/g\`, + } + + model \`C/at\` { + kind: "cat"; + meow: boolean; + } + model \`Do/g\` { + kind: "dog"; + bark: boolean; + } + + op f(p: \`Pe/t\`): void; + }`, + diagChecks: [ + { + expectedDiagInvalidKey: "C/at", + expectedDeclKey: "C_at", + expectedPrefix: "#/components/schemas/", + kind: "Model", + }, + { + expectedDiagInvalidKey: "Do/g", + expectedDeclKey: "Do_g", + expectedPrefix: "#/components/schemas/", + kind: "Model", + }, + { + expectedDiagInvalidKey: "Pe/t", + expectedDeclKey: "Pe_t", + expectedPrefix: "#/components/schemas/", + kind: "Union", + }, + ], + }, + { + title: "Basic extend case", + code: ` + @service + namespace NS { + @discriminator("kind") + union \`Pe/t\` { + cat: \`C/at\`, + dog: \`Do/g\`, + } + + model \`C/at\` { + kind: "cat"; + meow: boolean; + } + model \`Do/g\` { + kind: "dog"; + bark: boolean; + } + + op f(p: \`Pe/t\`): void; + }`, + diagChecks: [ + { + expectedDiagInvalidKey: "C/at", + expectedDeclKey: "C_at", + expectedPrefix: "#/components/schemas/", + kind: "Model", + }, + { + expectedDiagInvalidKey: "Do/g", + expectedDeclKey: "Do_g", + expectedPrefix: "#/components/schemas/", + kind: "Model", + }, + { + expectedDiagInvalidKey: "Pe/t", + expectedDeclKey: "Pe_t", + expectedPrefix: "#/components/schemas/", + kind: "Union", + }, + ], + } ]; worksFor(["3.0.0", "3.1.0"], async (specHelpers) => { @@ -140,7 +227,7 @@ worksFor(["3.0.0", "3.1.0"], async (specHelpers) => { for (const [i, d] of c.diagChecks.entries()) { const target = diag[i].target as any; expect(target).toHaveProperty("kind"); - expect(target.kind).toBe(prefixToKindMap[d.expectedPrefix]); + expect(target.kind).toBe(d.kind); // check generated doc const componentField = getComponentField(doc, d.expectedPrefix); expect(componentField).toBeDefined(); From 4b5c92b93e9090586762d0ee56aeb5a27b516282 Mon Sep 17 00:00:00 2001 From: albertxavier100 Date: Tue, 25 Feb 2025 07:27:55 +0800 Subject: [PATCH 15/25] modify decl only, avoid impact other emitters --- packages/openapi3/src/openapi.ts | 2 -- packages/openapi3/src/schema-emitter.ts | 45 ++++--------------------- packages/openapi3/src/util.ts | 7 ---- 3 files changed, 7 insertions(+), 47 deletions(-) diff --git a/packages/openapi3/src/openapi.ts b/packages/openapi3/src/openapi.ts index 16881f0de5..8208f0c282 100644 --- a/packages/openapi3/src/openapi.ts +++ b/packages/openapi3/src/openapi.ts @@ -1574,8 +1574,6 @@ function createOAPIEmitter( ensureValidComponentFixedFieldKey( program, property, - () => undefined, - (_) => {}, () => key, (newKey) => { root.components!.parameters![newKey] = { ...param }; diff --git a/packages/openapi3/src/schema-emitter.ts b/packages/openapi3/src/schema-emitter.ts index e733262b50..80ef4c4979 100644 --- a/packages/openapi3/src/schema-emitter.ts +++ b/packages/openapi3/src/schema-emitter.ts @@ -191,15 +191,6 @@ export class OpenAPI3SchemaEmitterBase< const isMultipart = this.getContentType().startsWith("multipart/"); let name = isMultipart ? baseName + "MultiPart" : baseName; - ensureValidComponentFixedFieldKey( - program, - model, - () => model.name, - (newKey) => (model.name = newKey), - () => name, - (newKey) => (name = newKey), - ); - return this.#createDeclaration(model, name, this.applyConstraints(model, schema as any)); } @@ -474,15 +465,6 @@ export class OpenAPI3SchemaEmitterBase< enumDeclaration(en: Enum, name: string): EmitterOutput { let baseName = getOpenAPITypeName(this.emitter.getProgram(), en, this.#typeNameOptions()); - ensureValidComponentFixedFieldKey( - this.emitter.getProgram(), - en, - () => en.name, - (newKey) => (en.name = newKey), - () => baseName, - (newKey) => (baseName = newKey), - ); - return this.#createDeclaration(en, baseName, new ObjectBuilder(this.enumSchema(en))); } @@ -511,17 +493,6 @@ export class OpenAPI3SchemaEmitterBase< const schema = this.unionSchema(union); let baseName = getOpenAPITypeName(this.emitter.getProgram(), union, this.#typeNameOptions()); - ensureValidComponentFixedFieldKey( - this.emitter.getProgram(), - union, - () => union.name, - (newKey) => { - if (union.name) union.name = newKey; - }, - () => baseName, - (newKey) => (baseName = newKey), - ); - return this.#createDeclaration(union, baseName, schema); } @@ -606,15 +577,6 @@ export class OpenAPI3SchemaEmitterBase< const schema = this.#getSchemaForScalar(scalar); let baseName = getOpenAPITypeName(this.emitter.getProgram(), scalar, this.#typeNameOptions()); - ensureValidComponentFixedFieldKey( - this.emitter.getProgram(), - scalar, - () => scalar.name, - (newKey) => (scalar.name = newKey), - () => baseName, - (newKey) => (baseName = newKey), - ); - // Don't create a declaration for std types return isStd ? schema @@ -755,6 +717,13 @@ export class OpenAPI3SchemaEmitterBase< } #createDeclaration(type: Type, name: string, schema: ObjectBuilder) { + ensureValidComponentFixedFieldKey( + this.emitter.getProgram(), + type, + () => name, + (newKey) => (name = newKey), + ); + const refUrl = getRef(this.emitter.getProgram(), type); if (refUrl) { return { diff --git a/packages/openapi3/src/util.ts b/packages/openapi3/src/util.ts index c081f8a854..95234c6b05 100644 --- a/packages/openapi3/src/util.ts +++ b/packages/openapi3/src/util.ts @@ -163,8 +163,6 @@ export function isBytesKeptRaw(program: Program, type: Type) { export function ensureValidComponentFixedFieldKey( program: Program, type: Type, - getEntityKey: () => string | undefined, - setEntityKey: (newKey: string) => void, getDeclarationKey: () => string, setDeclarationKey: (newKey: string) => void, ): void { @@ -173,11 +171,6 @@ export function ensureValidComponentFixedFieldKey( reportInvalidKey(program, type, oldDeclarationKey); const newDeclarationKey = createValidKey(oldDeclarationKey); setDeclarationKey(newDeclarationKey); - const oldEntityKey = getEntityKey(); - if (oldEntityKey) { - const newEntityKey = createValidKey(oldEntityKey); - setEntityKey(newEntityKey); - } } function isValidComponentFixedFieldKey(key: string) { From 7ebed46b858f5bc8824f2ec33c176bca1f71f81d Mon Sep 17 00:00:00 2001 From: albertxavier100 Date: Tue, 25 Feb 2025 07:40:06 +0800 Subject: [PATCH 16/25] cleanup --- packages/openapi3/src/openapi.ts | 20 +++++++------------- packages/openapi3/src/schema-emitter.ts | 7 +------ packages/openapi3/src/util.ts | 13 +++++-------- 3 files changed, 13 insertions(+), 27 deletions(-) diff --git a/packages/openapi3/src/openapi.ts b/packages/openapi3/src/openapi.ts index 8208f0c282..197c714a40 100644 --- a/packages/openapi3/src/openapi.ts +++ b/packages/openapi3/src/openapi.ts @@ -1571,19 +1571,13 @@ function createOAPIEmitter( ); root.components!.parameters![key] = { ...param }; - ensureValidComponentFixedFieldKey( - program, - property, - () => key, - (newKey) => { - root.components!.parameters![newKey] = { ...param }; - delete root.components?.parameters![key]; - for (const key of Object.keys(param)) { - delete param[key]; - } - param.$ref = "#/components/parameters/" + newKey; - }, - ); + const validKey = ensureValidComponentFixedFieldKey(program, property, key); + root.components!.parameters![validKey] = { ...param }; + delete root.components?.parameters![key]; + for (const key of Object.keys(param)) { + delete param[key]; + } + param.$ref = "#/components/parameters/" + validKey; } } diff --git a/packages/openapi3/src/schema-emitter.ts b/packages/openapi3/src/schema-emitter.ts index 80ef4c4979..a83a80ccab 100644 --- a/packages/openapi3/src/schema-emitter.ts +++ b/packages/openapi3/src/schema-emitter.ts @@ -717,12 +717,7 @@ export class OpenAPI3SchemaEmitterBase< } #createDeclaration(type: Type, name: string, schema: ObjectBuilder) { - ensureValidComponentFixedFieldKey( - this.emitter.getProgram(), - type, - () => name, - (newKey) => (name = newKey), - ); + name = ensureValidComponentFixedFieldKey(this.emitter.getProgram(), type, name); const refUrl = getRef(this.emitter.getProgram(), type); if (refUrl) { diff --git a/packages/openapi3/src/util.ts b/packages/openapi3/src/util.ts index 95234c6b05..90bb5ddbde 100644 --- a/packages/openapi3/src/util.ts +++ b/packages/openapi3/src/util.ts @@ -163,14 +163,11 @@ export function isBytesKeptRaw(program: Program, type: Type) { export function ensureValidComponentFixedFieldKey( program: Program, type: Type, - getDeclarationKey: () => string, - setDeclarationKey: (newKey: string) => void, -): void { - const oldDeclarationKey = getDeclarationKey(); - if (isValidComponentFixedFieldKey(oldDeclarationKey)) return; - reportInvalidKey(program, type, oldDeclarationKey); - const newDeclarationKey = createValidKey(oldDeclarationKey); - setDeclarationKey(newDeclarationKey); + oldKey: string, +): string { + if (isValidComponentFixedFieldKey(oldKey)) return oldKey; + reportInvalidKey(program, type, oldKey); + return createValidKey(oldKey); } function isValidComponentFixedFieldKey(key: string) { From 5055226796d16ea8632abcf6187d45524754034b Mon Sep 17 00:00:00 2001 From: albertxavier100 Date: Tue, 25 Feb 2025 07:41:42 +0800 Subject: [PATCH 17/25] . --- packages/openapi3/src/openapi.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/packages/openapi3/src/openapi.ts b/packages/openapi3/src/openapi.ts index 197c714a40..810a22d440 100644 --- a/packages/openapi3/src/openapi.ts +++ b/packages/openapi3/src/openapi.ts @@ -812,7 +812,6 @@ function createOAPIEmitter( parameters: getEndpointParameters(parameters.parameters, visibility), responses: getResponses(operation, operation.responses, examples), }; - const currentTags = getAllTags(program, op); if (currentTags) { oai3Operation.tags = currentTags; @@ -1577,7 +1576,7 @@ function createOAPIEmitter( for (const key of Object.keys(param)) { delete param[key]; } - param.$ref = "#/components/parameters/" + validKey; + param.$ref = "#/components/parameters/" + encodeURIComponent(validKey); } } From c81cb94a6f8c9a442b28ea1814e87f0281a99c60 Mon Sep 17 00:00:00 2001 From: albertxavier100 Date: Tue, 25 Feb 2025 07:43:27 +0800 Subject: [PATCH 18/25] . --- packages/openapi3/src/schema-emitter.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/packages/openapi3/src/schema-emitter.ts b/packages/openapi3/src/schema-emitter.ts index a83a80ccab..44665dfbf4 100644 --- a/packages/openapi3/src/schema-emitter.ts +++ b/packages/openapi3/src/schema-emitter.ts @@ -492,7 +492,6 @@ export class OpenAPI3SchemaEmitterBase< unionDeclaration(union: Union, name: string): EmitterOutput { const schema = this.unionSchema(union); let baseName = getOpenAPITypeName(this.emitter.getProgram(), union, this.#typeNameOptions()); - return this.#createDeclaration(union, baseName, schema); } @@ -575,7 +574,7 @@ export class OpenAPI3SchemaEmitterBase< scalarDeclaration(scalar: Scalar, name: string): EmitterOutput { const isStd = isStdType(this.emitter.getProgram(), scalar); const schema = this.#getSchemaForScalar(scalar); - let baseName = getOpenAPITypeName(this.emitter.getProgram(), scalar, this.#typeNameOptions()); + const baseName = getOpenAPITypeName(this.emitter.getProgram(), scalar, this.#typeNameOptions()); // Don't create a declaration for std types return isStd From 692519f0dfd94368cdd0cf5f51d831295304d9e7 Mon Sep 17 00:00:00 2001 From: albertxavier100 Date: Tue, 25 Feb 2025 08:33:55 +0800 Subject: [PATCH 19/25] handle generic type --- packages/openapi3/src/schema-emitter.ts | 5 ++++- packages/openapi3/test/component.test.ts | 25 +++++++++++++++++++++++- 2 files changed, 28 insertions(+), 2 deletions(-) diff --git a/packages/openapi3/src/schema-emitter.ts b/packages/openapi3/src/schema-emitter.ts index 44665dfbf4..59ef15c162 100644 --- a/packages/openapi3/src/schema-emitter.ts +++ b/packages/openapi3/src/schema-emitter.ts @@ -716,7 +716,10 @@ export class OpenAPI3SchemaEmitterBase< } #createDeclaration(type: Type, name: string, schema: ObjectBuilder) { - name = ensureValidComponentFixedFieldKey(this.emitter.getProgram(), type, name); + const skipNameValidation = type.kind === "Model" && type.templateMapper !== undefined; + if (!skipNameValidation) { + name = ensureValidComponentFixedFieldKey(this.emitter.getProgram(), type, name); + } const refUrl = getRef(this.emitter.getProgram(), type); if (refUrl) { diff --git a/packages/openapi3/test/component.test.ts b/packages/openapi3/test/component.test.ts index ab0d78bf86..278a764dee 100644 --- a/packages/openapi3/test/component.test.ts +++ b/packages/openapi3/test/component.test.ts @@ -209,7 +209,30 @@ const testCases: Case[] = [ kind: "Union", }, ], - } + }, + { + title: "Basic generic case", + code: ` + @service + namespace NS { + model Foo {x: T;} + model \`x/x/x\` {a: string} + op read(x: \`x/x/x\`): Foo; + }`, + diagChecks: [ + { + expectedDiagInvalidKey: "x/x/x", + expectedDeclKey: "x_x_x", + expectedPrefix: "#/components/schemas/", + kind: "Model", + }, + ], + refChecks: (doc) => { + expect( + doc.paths["/"].post.requestBody.content["application/json"].schema.properties.x.$ref, + ).toBe("#/components/schemas/x_x_x"); + }, + }, ]; worksFor(["3.0.0", "3.1.0"], async (specHelpers) => { From c1fad11d09b9153aa633e0ea42abb9d760da4cad Mon Sep 17 00:00:00 2001 From: albertxavier100 Date: Tue, 25 Feb 2025 09:08:02 +0800 Subject: [PATCH 20/25] fix para --- packages/openapi3/src/openapi.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/packages/openapi3/src/openapi.ts b/packages/openapi3/src/openapi.ts index a0cf93d094..72fc56f009 100644 --- a/packages/openapi3/src/openapi.ts +++ b/packages/openapi3/src/openapi.ts @@ -1576,10 +1576,9 @@ function createOAPIEmitter( typeNameOptions, ); root.components!.parameters![key] = { ...param }; - const validKey = ensureValidComponentFixedFieldKey(program, property, key); root.components!.parameters![validKey] = { ...param }; - delete root.components?.parameters![key]; + if (validKey !== key) delete root.components?.parameters![key]; for (const key of Object.keys(param)) { delete param[key]; } From f772410a818a8107d94a9b70860dadd1c99d1d0d Mon Sep 17 00:00:00 2001 From: albertxavier100 Date: Tue, 25 Feb 2025 09:08:13 +0800 Subject: [PATCH 21/25] add extend test --- packages/openapi3/test/component.test.ts | 29 ++++++++---------------- 1 file changed, 9 insertions(+), 20 deletions(-) diff --git a/packages/openapi3/test/component.test.ts b/packages/openapi3/test/component.test.ts index 278a764dee..9aa60e550d 100644 --- a/packages/openapi3/test/component.test.ts +++ b/packages/openapi3/test/component.test.ts @@ -125,11 +125,11 @@ const testCases: Case[] = [ }, }, { - title: "Basic discriminator case", + title: "Basic discriminated case", code: ` @service namespace NS { - @discriminator("kind") + @discriminated("kind") union \`Pe/t\` { cat: \`C/at\`, dog: \`Do/g\`, @@ -173,32 +173,21 @@ const testCases: Case[] = [ @service namespace NS { @discriminator("kind") - union \`Pe/t\` { - cat: \`C/at\`, - dog: \`Do/g\`, - } - - model \`C/at\` { - kind: "cat"; - meow: boolean; - } - model \`Do/g\` { - kind: "dog"; - bark: boolean; - } - + model \`Pe/t\`{ kind: string } + model \`C*at\` extends \`Pe/t\` {kind: "cat", meow: boolean} + model \`D*og\` extends \`Pe/t\` {kind: "dog", bark: boolean} op f(p: \`Pe/t\`): void; }`, diagChecks: [ { - expectedDiagInvalidKey: "C/at", + expectedDiagInvalidKey: "C*at", expectedDeclKey: "C_at", expectedPrefix: "#/components/schemas/", kind: "Model", }, { - expectedDiagInvalidKey: "Do/g", - expectedDeclKey: "Do_g", + expectedDiagInvalidKey: "D*og", + expectedDeclKey: "D_og", expectedPrefix: "#/components/schemas/", kind: "Model", }, @@ -206,7 +195,7 @@ const testCases: Case[] = [ expectedDiagInvalidKey: "Pe/t", expectedDeclKey: "Pe_t", expectedPrefix: "#/components/schemas/", - kind: "Union", + kind: "Model", }, ], }, From 29217aff442b5ef8388323f81c349654f75d4789 Mon Sep 17 00:00:00 2001 From: albertxavier100 Date: Tue, 25 Feb 2025 09:16:32 +0800 Subject: [PATCH 22/25] add discriminated test --- packages/openapi3/test/component.test.ts | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/packages/openapi3/test/component.test.ts b/packages/openapi3/test/component.test.ts index 9aa60e550d..de93422ef7 100644 --- a/packages/openapi3/test/component.test.ts +++ b/packages/openapi3/test/component.test.ts @@ -129,7 +129,7 @@ const testCases: Case[] = [ code: ` @service namespace NS { - @discriminated("kind") + @discriminated() union \`Pe/t\` { cat: \`C/at\`, dog: \`Do/g\`, @@ -153,12 +153,24 @@ const testCases: Case[] = [ expectedPrefix: "#/components/schemas/", kind: "Model", }, + { + expectedDiagInvalidKey: "Pe/tCat", + expectedDeclKey: "Pe_tCat", + expectedPrefix: "#/components/schemas/", + kind: "Model", + }, { expectedDiagInvalidKey: "Do/g", expectedDeclKey: "Do_g", expectedPrefix: "#/components/schemas/", kind: "Model", }, + { + expectedDiagInvalidKey: "Pe/tDog", + expectedDeclKey: "Pe_tDog", + expectedPrefix: "#/components/schemas/", + kind: "Model", + }, { expectedDiagInvalidKey: "Pe/t", expectedDeclKey: "Pe_t", From c2984cb555bc2d6d0d67d86abf3529e4e315bf3a Mon Sep 17 00:00:00 2001 From: albertxavier100 Date: Tue, 25 Feb 2025 09:52:51 +0800 Subject: [PATCH 23/25] make spell check and lint happy --- packages/openapi3/src/schema-emitter.ts | 6 +++--- packages/openapi3/test/component.test.ts | 10 +++++----- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/packages/openapi3/src/schema-emitter.ts b/packages/openapi3/src/schema-emitter.ts index 5001c41313..0f8a6dc25d 100644 --- a/packages/openapi3/src/schema-emitter.ts +++ b/packages/openapi3/src/schema-emitter.ts @@ -235,7 +235,7 @@ export class OpenAPI3SchemaEmitterBase< const baseName = getOpenAPITypeName(program, model, this.#typeNameOptions()); const isMultipart = this.getContentType().startsWith("multipart/"); - let name = isMultipart ? baseName + "MultiPart" : baseName; + const name = isMultipart ? baseName + "MultiPart" : baseName; return this.#createDeclaration(model, name, this.applyConstraints(model, schema as any)); } @@ -507,7 +507,7 @@ export class OpenAPI3SchemaEmitterBase< } enumDeclaration(en: Enum, name: string): EmitterOutput { - let baseName = getOpenAPITypeName(this.emitter.getProgram(), en, this.#typeNameOptions()); + const baseName = getOpenAPITypeName(this.emitter.getProgram(), en, this.#typeNameOptions()); return this.#createDeclaration(en, baseName, new ObjectBuilder(this.enumSchema(en))); } @@ -535,7 +535,7 @@ export class OpenAPI3SchemaEmitterBase< unionDeclaration(union: Union, name: string): EmitterOutput { const schema = this.unionSchema(union); - let baseName = getOpenAPITypeName(this.emitter.getProgram(), union, this.#typeNameOptions()); + const baseName = getOpenAPITypeName(this.emitter.getProgram(), union, this.#typeNameOptions()); return this.#createDeclaration(union, baseName, schema); } diff --git a/packages/openapi3/test/component.test.ts b/packages/openapi3/test/component.test.ts index de93422ef7..c347d5bfd8 100644 --- a/packages/openapi3/test/component.test.ts +++ b/packages/openapi3/test/component.test.ts @@ -23,14 +23,14 @@ const testCases: Case[] = [ code: ` @service namespace Ns1Valid { - model \`foo-/inva*li\td\` {} - op f(p: \`foo-/inva*li\td\`): void; + model \`foo-/invalid*\td\` {} + op f(p: \`foo-/invalid*\td\`): void; }`, diagChecks: [ { - expectedDiagInvalidKey: "foo-/inva*li\td", - expectedDeclKey: "foo-_inva_li_d", + expectedDiagInvalidKey: "foo-/invalid*\td", + expectedDeclKey: "foo-_invalid__d", expectedPrefix: "#/components/schemas/", kind: "Model", }, @@ -38,7 +38,7 @@ const testCases: Case[] = [ refChecks: (doc) => { expect( doc.paths["/"].post.requestBody.content["application/json"].schema.properties.p.$ref, - ).toBe("#/components/schemas/foo-_inva_li_d"); + ).toBe("#/components/schemas/foo-_invalid__d"); }, }, { From 3798085aac600d5b2fabf2e8d0ea110eb23547ef Mon Sep 17 00:00:00 2001 From: albertxavier100 Date: Wed, 26 Feb 2025 08:57:22 +0800 Subject: [PATCH 24/25] TT --- packages/openapi3/src/openapi.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/openapi3/src/openapi.ts b/packages/openapi3/src/openapi.ts index 72fc56f009..8b57180fbd 100644 --- a/packages/openapi3/src/openapi.ts +++ b/packages/openapi3/src/openapi.ts @@ -1575,7 +1575,6 @@ function createOAPIEmitter( root.components!.parameters!, typeNameOptions, ); - root.components!.parameters![key] = { ...param }; const validKey = ensureValidComponentFixedFieldKey(program, property, key); root.components!.parameters![validKey] = { ...param }; if (validKey !== key) delete root.components?.parameters![key]; From 99eb692e3d3f250a711e544dc9d3a8c8c35cd195 Mon Sep 17 00:00:00 2001 From: albertxavier100 Date: Wed, 26 Feb 2025 10:41:54 +0800 Subject: [PATCH 25/25] remove --- packages/openapi3/src/openapi.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/openapi3/src/openapi.ts b/packages/openapi3/src/openapi.ts index 8b57180fbd..1b519d20d3 100644 --- a/packages/openapi3/src/openapi.ts +++ b/packages/openapi3/src/openapi.ts @@ -1577,7 +1577,6 @@ function createOAPIEmitter( ); const validKey = ensureValidComponentFixedFieldKey(program, property, key); root.components!.parameters![validKey] = { ...param }; - if (validKey !== key) delete root.components?.parameters![key]; for (const key of Object.keys(param)) { delete param[key]; }