From 5e539678994be3a7ff7e86eea2b2f9193c94c0e7 Mon Sep 17 00:00:00 2001 From: Timothee Guerin Date: Wed, 12 Jul 2023 11:47:49 -0400 Subject: [PATCH] Feature: Add new `@info` decorator (#2169) fix [#980](https://github.com/microsoft/typespec/issues/980) Add new `@info` decorator providing the ability to specify the additional fields from openapi info object. --------- Co-authored-by: Brian Terlson --- ...feature-openapi-info_2023-07-11-15-24.json | 10 ++++ ...feature-openapi-info_2023-07-11-15-24.json | 10 ++++ .../openapi/reference/data-types.md | 33 ++++++++++ .../openapi/reference/decorators.md | 19 ++++++ .../openapi/reference/index.md | 7 +++ packages/openapi/lib/decorators.tsp | 40 +++++++++++++ packages/openapi/src/decorators.ts | 14 ++++- packages/openapi/src/types.ts | 33 ++++++++++ packages/openapi/test/decorators.test.ts | 60 ++++++++++++++++++- packages/openapi3/src/openapi.ts | 2 + packages/openapi3/test/info.test.ts | 37 ++++++++++++ 11 files changed, 263 insertions(+), 2 deletions(-) create mode 100644 common/changes/@typespec/openapi/feature-openapi-info_2023-07-11-15-24.json create mode 100644 common/changes/@typespec/openapi3/feature-openapi-info_2023-07-11-15-24.json create mode 100644 docs/standard-library/openapi/reference/data-types.md diff --git a/common/changes/@typespec/openapi/feature-openapi-info_2023-07-11-15-24.json b/common/changes/@typespec/openapi/feature-openapi-info_2023-07-11-15-24.json new file mode 100644 index 0000000000..bb1c3c56c0 --- /dev/null +++ b/common/changes/@typespec/openapi/feature-openapi-info_2023-07-11-15-24.json @@ -0,0 +1,10 @@ +{ + "changes": [ + { + "packageName": "@typespec/openapi", + "comment": "Add new `@info` decorator providing the ability to specify the additional fields from openapi info object.", + "type": "none" + } + ], + "packageName": "@typespec/openapi" +} \ No newline at end of file diff --git a/common/changes/@typespec/openapi3/feature-openapi-info_2023-07-11-15-24.json b/common/changes/@typespec/openapi3/feature-openapi-info_2023-07-11-15-24.json new file mode 100644 index 0000000000..3f9f2b59b6 --- /dev/null +++ b/common/changes/@typespec/openapi3/feature-openapi-info_2023-07-11-15-24.json @@ -0,0 +1,10 @@ +{ + "changes": [ + { + "packageName": "@typespec/openapi3", + "comment": "Add support for `@info` decorator providing the ability to specify the additional fields from openapi info object.", + "type": "none" + } + ], + "packageName": "@typespec/openapi3" +} diff --git a/docs/standard-library/openapi/reference/data-types.md b/docs/standard-library/openapi/reference/data-types.md new file mode 100644 index 0000000000..476b98eae0 --- /dev/null +++ b/docs/standard-library/openapi/reference/data-types.md @@ -0,0 +1,33 @@ +--- +title: "Data types" +toc_min_heading_level: 2 +toc_max_heading_level: 3 +--- + +# Data types + +## OpenAPI + +### `AdditionalInfo` {#OpenAPI.AdditionalInfo} + +Additional information for the OpenAPI document. + +```typespec +model OpenAPI.AdditionalInfo +``` + +### `Contact` {#OpenAPI.Contact} + +Contact information for the exposed API. + +```typespec +model OpenAPI.Contact +``` + +### `License` {#OpenAPI.License} + +License information for the exposed API. + +```typespec +model OpenAPI.License +``` diff --git a/docs/standard-library/openapi/reference/decorators.md b/docs/standard-library/openapi/reference/decorators.md index 3e93f64ce6..d9c143db57 100644 --- a/docs/standard-library/openapi/reference/decorators.md +++ b/docs/standard-library/openapi/reference/decorators.md @@ -87,6 +87,25 @@ Specify the OpenAPI `externalDocs` property for this type. op listPets(): Pet[]; ``` +### `@info` {#@OpenAPI.info} + +Specify OpenAPI additional information. +The service `title` and `version` are already specified using `@service`. + +```typespec +@OpenAPI.info(additionalInfo: OpenAPI.AdditionalInfo) +``` + +#### Target + +`Namespace` + +#### Parameters + +| Name | Type | Description | +| -------------- | ------------------------------ | ---------------------- | +| additionalInfo | `model OpenAPI.AdditionalInfo` | Additional information | + ### `@operationId` {#@OpenAPI.operationId} Specify the OpenAPI `operationId` property for this operation. diff --git a/docs/standard-library/openapi/reference/index.md b/docs/standard-library/openapi/reference/index.md index d34687db87..064b569995 100644 --- a/docs/standard-library/openapi/reference/index.md +++ b/docs/standard-library/openapi/reference/index.md @@ -36,4 +36,11 @@ npm install --save-peer @typespec/openapi - [`@defaultResponse`](./decorators.md#@OpenAPI.defaultResponse) - [`@extension`](./decorators.md#@OpenAPI.extension) - [`@externalDocs`](./decorators.md#@OpenAPI.externalDocs) +- [`@info`](./decorators.md#@OpenAPI.info) - [`@operationId`](./decorators.md#@OpenAPI.operationId) + +### Models + +- [`AdditionalInfo`](./data-types.md#OpenAPI.AdditionalInfo) +- [`Contact`](./data-types.md#OpenAPI.Contact) +- [`License`](./data-types.md#OpenAPI.License) diff --git a/packages/openapi/lib/decorators.tsp b/packages/openapi/lib/decorators.tsp index 9708e4487f..53dde05e33 100644 --- a/packages/openapi/lib/decorators.tsp +++ b/packages/openapi/lib/decorators.tsp @@ -60,3 +60,43 @@ extern dec defaultResponse(target: Model); * ``` */ extern dec externalDocs(target: unknown, url: valueof string, description?: valueof string); + +/** Additional information for the OpenAPI document. */ +model AdditionalInfo { + /** A URL to the Terms of Service for the API. MUST be in the format of a URL. */ + termsOfService?: url; + + /** The contact information for the exposed API. */ + contact?: Contact; + + /** The license information for the exposed API. */ + license?: License; +} + +/** Contact information for the exposed API. */ +model Contact { + /** The identifying name of the contact person/organization. */ + name?: string; + + /** The URL pointing to the contact information. MUST be in the format of a URL. */ + url?: url; + + /** The email address of the contact person/organization. MUST be in the format of an email address. */ + email?: string; +} + +/** License information for the exposed API. */ +model License { + /** The license name used for the API. */ + name: string; + + /** A URL to the license used for the API. MUST be in the format of a URL. */ + url?: url; +} + +/** + * Specify OpenAPI additional information. + * The service `title` and `version` are already specified using `@service`. + * @param additionalInfo Additional information + */ +extern dec info(target: Namespace, additionalInfo: AdditionalInfo); diff --git a/packages/openapi/src/decorators.ts b/packages/openapi/src/decorators.ts index d4e4953dbb..c5f100e7e9 100644 --- a/packages/openapi/src/decorators.ts +++ b/packages/openapi/src/decorators.ts @@ -1,6 +1,7 @@ import { DecoratorContext, Model, + Namespace, Operation, Program, Type, @@ -9,7 +10,7 @@ import { } from "@typespec/compiler"; import { setStatusCode } from "@typespec/http"; import { createStateSymbol, reportDiagnostic } from "./lib.js"; -import { ExtensionKey } from "./types.js"; +import { AdditionalInfo, ExtensionKey } from "./types.js"; export const namespace = "OpenAPI"; @@ -123,3 +124,14 @@ export function $externalDocs( export function getExternalDocs(program: Program, entity: Type): ExternalDocs | undefined { return program.stateMap(externalDocsKey).get(entity); } + +const infoKey = createStateSymbol("info"); +export function $info(context: DecoratorContext, entity: Namespace, model: Model) { + const [data, diagnostics] = typespecTypeToJson(model, context.getArgumentTarget(0)!); + context.program.reportDiagnostics(diagnostics); + context.program.stateMap(infoKey).set(entity, data); +} + +export function getInfo(program: Program, entity: Namespace): AdditionalInfo | undefined { + return program.stateMap(infoKey).get(entity); +} diff --git a/packages/openapi/src/types.ts b/packages/openapi/src/types.ts index 0bddcb7621..d683789f1b 100644 --- a/packages/openapi/src/types.ts +++ b/packages/openapi/src/types.ts @@ -1 +1,34 @@ export type ExtensionKey = `x-${string}`; + +/** + * OpenAPI additional information + */ +export interface AdditionalInfo { + /** A URL to the Terms of Service for the API. MUST be in the format of a URL. */ + termsOfService?: string; + + /** The contact information for the exposed API. */ + contact?: Contact; + + /** The license information for the exposed API. */ + license?: License; +} + +export interface Contact { + /** The identifying name of the contact person/organization. */ + name?: string; + + /** The URL pointing to the contact information. MUST be in the format of a URL. */ + url?: string; + + /** The email address of the contact person/organization. MUST be in the format of an email address. */ + email?: string; +} + +export interface License { + /** The license name used for the API. */ + name: string; + + /** A URL to the license used for the API. MUST be in the format of a URL. */ + url?: string; +} diff --git a/packages/openapi/test/decorators.test.ts b/packages/openapi/test/decorators.test.ts index 4a923ca91d..d6b608b92b 100644 --- a/packages/openapi/test/decorators.test.ts +++ b/packages/openapi/test/decorators.test.ts @@ -1,6 +1,7 @@ +import { Namespace } from "@typespec/compiler"; import { BasicTestRunner, expectDiagnostics } from "@typespec/compiler/testing"; import { deepStrictEqual } from "assert"; -import { getExtensions, getExternalDocs } from "../src/decorators.js"; +import { getExtensions, getExternalDocs, getInfo } from "../src/decorators.js"; import { createOpenAPITestRunner } from "./test-host.js"; describe("openapi: decorators", () => { @@ -146,4 +147,61 @@ describe("openapi: decorators", () => { }); }); }); + + describe("@info", () => { + it("emit diagnostic if use on non namespace", async () => { + const diagnostics = await runner.diagnose(` + @info({}) + model Foo {} + `); + + expectDiagnostics(diagnostics, { + code: "decorator-wrong-target", + message: "Cannot apply @info decorator to Foo since it is not assignable to Namespace", + }); + }); + + it("emit diagnostic if info parameter is not an object", async () => { + const diagnostics = await runner.diagnose(` + @info(123) + namespace Service {} + `); + + expectDiagnostics(diagnostics, { + code: "invalid-argument", + message: "Argument '123' is not assignable to parameter of type 'OpenAPI.AdditionalInfo'", + }); + }); + + it("set all properties", async () => { + const { Service } = (await runner.compile(` + @info({ + termsOfService: "http://example.com/terms/", + contact: { + name: "API Support", + url: "http://www.example.com/support", + email: "support@example.com" + }, + license: { + name: "Apache 2.0", + url: "http://www.apache.org/licenses/LICENSE-2.0.html" + }, + }) + @test namespace Service {} + `)) as { Service: Namespace }; + + deepStrictEqual(getInfo(runner.program, Service), { + termsOfService: "http://example.com/terms/", + contact: { + name: "API Support", + url: "http://www.example.com/support", + email: "support@example.com", + }, + license: { + name: "Apache 2.0", + url: "http://www.apache.org/licenses/LICENSE-2.0.html", + }, + }); + }); + }); }); diff --git a/packages/openapi3/src/openapi.ts b/packages/openapi3/src/openapi.ts index cdba5840c5..e74f6feddd 100644 --- a/packages/openapi3/src/openapi.ts +++ b/packages/openapi3/src/openapi.ts @@ -89,6 +89,7 @@ import { checkDuplicateTypeName, getExtensions, getExternalDocs, + getInfo, getOpenAPITypeName, getParameterKey, isReadonlyProperty, @@ -262,6 +263,7 @@ function createOAPIEmitter(program: Program, options: ResolvedOpenAPI3EmitterOpt title: service.title ?? "(title)", version: version ?? service.version ?? "0000-00-00", description: getDoc(program, service.type), + ...getInfo(program, service.type), }, externalDocs: getExternalDocs(program, service.type), tags: [], diff --git a/packages/openapi3/test/info.test.ts b/packages/openapi3/test/info.test.ts index 64964d8270..87ea0d2187 100644 --- a/packages/openapi3/test/info.test.ts +++ b/packages/openapi3/test/info.test.ts @@ -53,4 +53,41 @@ describe("openapi3: info", () => { description: "more info", }); }); + + it("set the additional information with @info decorator", async () => { + const res = await openApiFor( + ` + @service + @info({ + termsOfService: "http://example.com/terms/", + contact: { + name: "API Support", + url: "http://www.example.com/support", + email: "support@example.com" + }, + license: { + name: "Apache 2.0", + url: "http://www.apache.org/licenses/LICENSE-2.0.html" + }, + }) + namespace Foo { + op test(): string; + } + ` + ); + deepStrictEqual(res.info, { + title: "(title)", + version: "0000-00-00", + termsOfService: "http://example.com/terms/", + contact: { + name: "API Support", + url: "http://www.example.com/support", + email: "support@example.com", + }, + license: { + name: "Apache 2.0", + url: "http://www.apache.org/licenses/LICENSE-2.0.html", + }, + }); + }); });