From dde8dc0ca70578e81bcd435f09e03bb8d84bd082 Mon Sep 17 00:00:00 2001 From: Timothee Guerin Date: Fri, 6 Sep 2024 16:18:53 -0700 Subject: [PATCH] Add api extractor for openapi package and fix issues (#4354) --- .../api-extractor-openapi-2024-8-5-13-50-8.md | 7 +++ packages/openapi/api-extractor.json | 4 ++ packages/openapi/package.json | 5 +- packages/openapi/src/decorators.ts | 52 ++++++++++++------- packages/openapi/src/helpers.ts | 9 +++- packages/openapi/src/index.ts | 36 +++++++++++-- packages/openapi/src/types.ts | 20 +++++++ 7 files changed, 109 insertions(+), 24 deletions(-) create mode 100644 .chronus/changes/api-extractor-openapi-2024-8-5-13-50-8.md create mode 100644 packages/openapi/api-extractor.json diff --git a/.chronus/changes/api-extractor-openapi-2024-8-5-13-50-8.md b/.chronus/changes/api-extractor-openapi-2024-8-5-13-50-8.md new file mode 100644 index 000000000..7744eddc2 --- /dev/null +++ b/.chronus/changes/api-extractor-openapi-2024-8-5-13-50-8.md @@ -0,0 +1,7 @@ +--- +changeKind: internal +packages: + - "@typespec/openapi" +--- + +Add api extractor setup \ No newline at end of file diff --git a/packages/openapi/api-extractor.json b/packages/openapi/api-extractor.json new file mode 100644 index 000000000..2069b8ac3 --- /dev/null +++ b/packages/openapi/api-extractor.json @@ -0,0 +1,4 @@ +{ + "$schema": "https://developer.microsoft.com/json-schemas/api-extractor/v7/api-extractor.schema.json", + "extends": "../../api-extractor.base.json" +} diff --git a/packages/openapi/package.json b/packages/openapi/package.json index 932ebb826..831b3f62b 100644 --- a/packages/openapi/package.json +++ b/packages/openapi/package.json @@ -34,7 +34,7 @@ }, "scripts": { "clean": "rimraf ./dist ./temp", - "build": "npm run gen-extern-signature && tsc -p . && npm run lint-typespec-library", + "build": "pnpm gen-extern-signature && tsc -p . && pnpm lint-typespec-library && pnpm api-extractor", "watch": "tsc -p . --watch", "gen-extern-signature": "tspd --enable-experimental gen-extern-signature .", "lint-typespec-library": "tsp compile . --warn-as-error --import @typespec/library-linter --no-emit", @@ -44,7 +44,8 @@ "test:ci": "vitest run --coverage --reporter=junit --reporter=default", "lint": "eslint . --max-warnings=0", "lint:fix": "eslint . --fix", - "regen-docs": "tspd doc . --enable-experimental --output-dir ../../docs/libraries/openapi/reference" + "regen-docs": "tspd doc . --enable-experimental --output-dir ../../docs/libraries/openapi/reference", + "api-extractor": "api-extractor run --local --verbose" }, "files": [ "lib/*.tsp", diff --git a/packages/openapi/src/decorators.ts b/packages/openapi/src/decorators.ts index d080e3732..b481cb94a 100644 --- a/packages/openapi/src/decorators.ts +++ b/packages/openapi/src/decorators.ts @@ -18,10 +18,9 @@ import { ExternalDocsDecorator, InfoDecorator, OperationIdDecorator, - TypeSpecOpenAPIDecorators, } from "../generated-defs/TypeSpec.OpenAPI.js"; import { createStateSymbol, reportDiagnostic } from "./lib.js"; -import { AdditionalInfo, ExtensionKey } from "./types.js"; +import { AdditionalInfo, ExtensionKey, ExternalDocs } from "./types.js"; const operationIdsKey = createStateSymbol("operationIds"); /** @@ -39,7 +38,7 @@ export const $operationId: OperationIdDecorator = ( }; /** - * @returns operationId set via the @operationId decorator or `undefined` + * Returns operationId set via the `@operationId` decorator or `undefined` */ export function getOperationId(program: Program, entity: Operation): string | undefined { return program.stateMap(operationIdsKey).get(entity); @@ -47,6 +46,7 @@ export function getOperationId(program: Program, entity: Operation): string | un const openApiExtensionKey = createStateSymbol("openApiExtension"); +/** {@inheritdoc ExtensionDecorator} */ export const $extension: ExtensionDecorator = ( context: DecoratorContext, entity: Type, @@ -68,6 +68,12 @@ export const $extension: ExtensionDecorator = ( setExtension(context.program, entity, extensionName as ExtensionKey, data); }; +/** + * Set the OpenAPI info node on for the given service namespace. + * @param program Program + * @param entity Service namespace + * @param data OpenAPI Info object + */ export function setInfo( program: Program, entity: Namespace, @@ -76,6 +82,13 @@ export function setInfo( program.stateMap(infoKey).set(entity, data); } +/** + * Set OpenAPI extension on the given type. Equivalent of using `@extension` decorator + * @param program Program + * @param entity Type to annotate + * @param extensionName Extension key + * @param data Extension value + */ export function setExtension( program: Program, entity: Type, @@ -88,6 +101,11 @@ export function setExtension( openApiExtensions.set(entity, typeExtensions); } +/** + * Get extensions set for the given type. + * @param program Program + * @param entity Type + */ export function getExtensions(program: Program, entity: Type): ReadonlyMap { return program.stateMap(openApiExtensionKey).get(entity) ?? new Map(); } @@ -102,6 +120,7 @@ function isOpenAPIExtensionKey(key: string): key is ExtensionKey { * */ const defaultResponseKey = createStateSymbol("defaultResponse"); +/** {@inheritdoc DefaultResponseDecorator} */ export const $defaultResponse: DefaultResponseDecorator = ( context: DecoratorContext, entity: Model @@ -121,10 +140,6 @@ export function isDefaultResponse(program: Program, entity: Type): boolean { return program.stateSet(defaultResponseKey).has(entity); } -export interface ExternalDocs { - url: string; - description?: string; -} const externalDocsKey = createStateSymbol("externalDocs"); /** @@ -145,11 +160,18 @@ export const $externalDocs: ExternalDocsDecorator = ( context.program.stateMap(externalDocsKey).set(target, doc); }; +/** + * Return external doc info set via the `@externalDocs` decorator. + * @param program Program + * @param entity Type + */ export function getExternalDocs(program: Program, entity: Type): ExternalDocs | undefined { return program.stateMap(externalDocsKey).get(entity); } const infoKey = createStateSymbol("info"); + +/** {@inheritdoc InfoDecorator} */ export const $info: InfoDecorator = ( context: DecoratorContext, entity: Namespace, @@ -166,6 +188,11 @@ export const $info: InfoDecorator = ( setInfo(context.program, entity, data); }; +/** + * Get the info entry for the given service namespace. + * @param program Program + * @param entity Service namespace + */ export function getInfo(program: Program, entity: Namespace): AdditionalInfo | undefined { return program.stateMap(infoKey).get(entity); } @@ -187,14 +214,3 @@ export function resolveInfo(program: Program, entity: Namespace): AdditionalInfo function omitUndefined>(data: T): T { return Object.fromEntries(Object.entries(data).filter(([k, v]) => v !== undefined)) as any; } - -/** @internal */ -export const $decorators = { - "TypeSpec.OpenAPI": { - defaultResponse: $defaultResponse, - extension: $extension, - externalDocs: $externalDocs, - info: $info, - operationId: $operationId, - } satisfies TypeSpecOpenAPIDecorators, -}; diff --git a/packages/openapi/src/helpers.ts b/packages/openapi/src/helpers.ts index f19ff3e52..8be906458 100644 --- a/packages/openapi/src/helpers.ts +++ b/packages/openapi/src/helpers.ts @@ -64,6 +64,13 @@ export function getOpenAPITypeName( return name; } +/** + * Check the given name is not already specific in the existing map. Report a diagnostic if it is. + * @param program Program + * @param type Type with the name to check + * @param name Name to check + * @param existing Existing map of name + */ export function checkDuplicateTypeName( program: Program, type: Type, @@ -114,7 +121,7 @@ export function getParameterKey( /** * Resolve the OpenAPI operation ID for the given operation using the following logic: - * - If @operationId was specified use that value + * - If `@operationId` was specified use that value * - If operation is defined at the root or under the service namespace return `` * - Otherwise(operation is under another namespace or interface) return `_` * diff --git a/packages/openapi/src/index.ts b/packages/openapi/src/index.ts index 3f664aaf2..38e073db5 100644 --- a/packages/openapi/src/index.ts +++ b/packages/openapi/src/index.ts @@ -1,3 +1,33 @@ -export * from "./decorators.js"; -export * from "./helpers.js"; -export * from "./types.js"; +export type { + DefaultResponseDecorator, + ExtensionDecorator, + ExternalDocsDecorator, + InfoDecorator, +} from "../generated-defs/TypeSpec.OpenAPI.js"; +export { + $defaultResponse, + $extension, + $externalDocs, + $info, + $operationId, + getExtensions, + getExternalDocs, + getInfo, + getOperationId, + isDefaultResponse, + resolveInfo, + setExtension, + setInfo, +} from "./decorators.js"; +export { + checkDuplicateTypeName, + getOpenAPITypeName, + getParameterKey, + isReadonlyProperty, + resolveOperationId, + shouldInline, +} from "./helpers.js"; +export { AdditionalInfo, Contact, ExtensionKey, ExternalDocs, License } from "./types.js"; + +/** @internal */ +export { $decorators } from "./tsp-index.js"; diff --git a/packages/openapi/src/types.ts b/packages/openapi/src/types.ts index 6445a4456..9a8b600f9 100644 --- a/packages/openapi/src/types.ts +++ b/packages/openapi/src/types.ts @@ -1,3 +1,7 @@ +/** + * Pattern for extension keys. + * In OpenAPI only unknown properties starting with `x-` are allowed. + */ export type ExtensionKey = `x-${string}`; /** @@ -26,6 +30,9 @@ export interface AdditionalInfo { license?: License; } +/** + * Contact information + */ export interface Contact { /** The identifying name of the contact person/organization. */ name?: string; @@ -37,6 +44,9 @@ export interface Contact { email?: string; } +/** + * License information + */ export interface License { /** The license name used for the API. */ name: string; @@ -44,3 +54,13 @@ export interface License { /** A URL to the license used for the API. MUST be in the format of a URL. */ url?: string; } + +/** + * External Docs info + */ +export interface ExternalDocs { + /** Documentation url */ + url: string; + /** Optional description */ + description?: string; +}