diff --git a/.chronus/changes/checkAvailableTypeName-2024-10-7-15-22-31.md b/.chronus/changes/checkAvailableTypeName-2024-10-7-15-22-31.md new file mode 100644 index 00000000000..ca3ef477717 --- /dev/null +++ b/.chronus/changes/checkAvailableTypeName-2024-10-7-15-22-31.md @@ -0,0 +1,7 @@ +--- +changeKind: fix +packages: + - "@typespec/openapi3" +--- + +Illegal characters in component keys diff --git a/packages/openapi3/src/lib.ts b/packages/openapi3/src/lib.ts index b8a8c9e0bcc..bd994c9a85c 100644 --- a/packages/openapi3/src/lib.ts +++ b/packages/openapi3/src/lib.ts @@ -275,6 +275,12 @@ export const libDef = { default: paramMessage`XML \`@unwrapped\` can only used on array properties or primitive ones in the OpenAPI 3 emitter, Property '${"name"}' will be ignored.`, }, }, + "invalid-component-fixed-field-key": { + severity: "error", + 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.`, + }, + }, }, emitter: { options: EmitterOptionsSchema as JSONSchemaType, diff --git a/packages/openapi3/src/openapi.ts b/packages/openapi3/src/openapi.ts index 6659ccdb194..fa533c0e0c1 100644 --- a/packages/openapi3/src/openapi.ts +++ b/packages/openapi3/src/openapi.ts @@ -1540,6 +1540,21 @@ 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( @@ -1549,6 +1564,7 @@ function createOAPIEmitter( root.components!.parameters!, typeNameOptions, ); + validateComponentFixedFieldKey(property, key); root.components!.parameters![key] = { ...param }; for (const key of Object.keys(param)) { @@ -1573,6 +1589,8 @@ 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/test/models.test.ts b/packages/openapi3/test/models.test.ts index 70c198105fe..774c7deec96 100644 --- a/packages/openapi3/test/models.test.ts +++ b/packages/openapi3/test/models.test.ts @@ -102,6 +102,60 @@ describe("openapi3: models", () => { ]); }); + describe("errors on invalid model names", () => { + const symbols = [ + "!", + "@", + "#", + "$", + "%", + "^", + "&", + "*", + "(", + ")", + "=", + "+", + "[", + "]", + "{", + "}", + "|", + ";", + ":", + "<", + ">", + ",", + "/", + "?", + "~", + ]; + it.each(symbols)("%sName01", async (model) => { + const diagnostics = await diagnoseOpenApiFor( + ` + model \`${model}Name01\` { name: string; } + `, + ); + expectDiagnostics(diagnostics, [ + { + code: "@typespec/openapi3/invalid-component-fixed-field-key", + }, + ]); + }); + }); + + describe("no errors on valid model names", () => { + const symbols = [".", "-", "_"]; + it.each(symbols)("%sName01", async (model) => { + const diagnostics = await diagnoseOpenApiFor( + ` + model \`${model}Name01\` { name: string; } + `, + ); + expectDiagnostics(diagnostics, []); + }); + }); + it("doesn't define anonymous models", async () => { const res = await oapiForModel("{ x: int32 }", ""); diff --git a/packages/openapi3/test/parameters.test.ts b/packages/openapi3/test/parameters.test.ts index 4f7454f78a6..d646df97bbb 100644 --- a/packages/openapi3/test/parameters.test.ts +++ b/packages/openapi3/test/parameters.test.ts @@ -301,27 +301,32 @@ describe("query parameters", () => { ]); }); - it("encodes parameter keys in references", async () => { - const oapi = await openApiFor(` - model Pet extends Pet$Id { - name: string; + it("errors on invalid parameter keys", async () => { + const diagnostics = await diagnoseOpenApiFor( + ` + model Pet { + @query() + $take?: int32; + + @query() + $top?: int32; } - model Pet$Id { - @path - petId: string; + @service + namespace Endpoints { + op list(...Pet): void; } - - @route("/Pets") - @get() - op get(... Pet$Id): Pet; - `); - - ok(oapi.paths["/Pets/{petId}"].get); - strictEqual( - oapi.paths["/Pets/{petId}"].get.parameters[0]["$ref"], - "#/components/parameters/Pet%24Id", + `, + { "omit-unreachable-types": true }, ); - strictEqual(oapi.components.parameters["Pet$Id"].name, "petId"); + + expectDiagnostics(diagnostics, [ + { + code: "@typespec/openapi3/invalid-component-fixed-field-key", + }, + { + code: "@typespec/openapi3/invalid-component-fixed-field-key", + }, + ]); }); it("inline spread of parameters from anonymous model", async () => {