Add api extractor for openapi package and fix issues (#4354)

This commit is contained in:
Timothee Guerin 2024-09-06 16:18:53 -07:00 коммит произвёл GitHub
Родитель 86cc361bf0
Коммит dde8dc0ca7
Не найден ключ, соответствующий данной подписи
Идентификатор ключа GPG: B5690EEEBB952194
7 изменённых файлов: 109 добавлений и 24 удалений

Просмотреть файл

@ -0,0 +1,7 @@
---
changeKind: internal
packages:
- "@typespec/openapi"
---
Add api extractor setup

Просмотреть файл

@ -0,0 +1,4 @@
{
"$schema": "https://developer.microsoft.com/json-schemas/api-extractor/v7/api-extractor.schema.json",
"extends": "../../api-extractor.base.json"
}

Просмотреть файл

@ -34,7 +34,7 @@
}, },
"scripts": { "scripts": {
"clean": "rimraf ./dist ./temp", "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", "watch": "tsc -p . --watch",
"gen-extern-signature": "tspd --enable-experimental gen-extern-signature .", "gen-extern-signature": "tspd --enable-experimental gen-extern-signature .",
"lint-typespec-library": "tsp compile . --warn-as-error --import @typespec/library-linter --no-emit", "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", "test:ci": "vitest run --coverage --reporter=junit --reporter=default",
"lint": "eslint . --max-warnings=0", "lint": "eslint . --max-warnings=0",
"lint:fix": "eslint . --fix", "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": [ "files": [
"lib/*.tsp", "lib/*.tsp",

Просмотреть файл

@ -18,10 +18,9 @@ import {
ExternalDocsDecorator, ExternalDocsDecorator,
InfoDecorator, InfoDecorator,
OperationIdDecorator, OperationIdDecorator,
TypeSpecOpenAPIDecorators,
} from "../generated-defs/TypeSpec.OpenAPI.js"; } from "../generated-defs/TypeSpec.OpenAPI.js";
import { createStateSymbol, reportDiagnostic } from "./lib.js"; import { createStateSymbol, reportDiagnostic } from "./lib.js";
import { AdditionalInfo, ExtensionKey } from "./types.js"; import { AdditionalInfo, ExtensionKey, ExternalDocs } from "./types.js";
const operationIdsKey = createStateSymbol("operationIds"); 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 { export function getOperationId(program: Program, entity: Operation): string | undefined {
return program.stateMap(operationIdsKey).get(entity); return program.stateMap(operationIdsKey).get(entity);
@ -47,6 +46,7 @@ export function getOperationId(program: Program, entity: Operation): string | un
const openApiExtensionKey = createStateSymbol("openApiExtension"); const openApiExtensionKey = createStateSymbol("openApiExtension");
/** {@inheritdoc ExtensionDecorator} */
export const $extension: ExtensionDecorator = ( export const $extension: ExtensionDecorator = (
context: DecoratorContext, context: DecoratorContext,
entity: Type, entity: Type,
@ -68,6 +68,12 @@ export const $extension: ExtensionDecorator = (
setExtension(context.program, entity, extensionName as ExtensionKey, data); 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( export function setInfo(
program: Program, program: Program,
entity: Namespace, entity: Namespace,
@ -76,6 +82,13 @@ export function setInfo(
program.stateMap(infoKey).set(entity, data); 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( export function setExtension(
program: Program, program: Program,
entity: Type, entity: Type,
@ -88,6 +101,11 @@ export function setExtension(
openApiExtensions.set(entity, typeExtensions); 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<ExtensionKey, any> { export function getExtensions(program: Program, entity: Type): ReadonlyMap<ExtensionKey, any> {
return program.stateMap(openApiExtensionKey).get(entity) ?? new Map<ExtensionKey, any>(); return program.stateMap(openApiExtensionKey).get(entity) ?? new Map<ExtensionKey, any>();
} }
@ -102,6 +120,7 @@ function isOpenAPIExtensionKey(key: string): key is ExtensionKey {
* *
*/ */
const defaultResponseKey = createStateSymbol("defaultResponse"); const defaultResponseKey = createStateSymbol("defaultResponse");
/** {@inheritdoc DefaultResponseDecorator} */
export const $defaultResponse: DefaultResponseDecorator = ( export const $defaultResponse: DefaultResponseDecorator = (
context: DecoratorContext, context: DecoratorContext,
entity: Model entity: Model
@ -121,10 +140,6 @@ export function isDefaultResponse(program: Program, entity: Type): boolean {
return program.stateSet(defaultResponseKey).has(entity); return program.stateSet(defaultResponseKey).has(entity);
} }
export interface ExternalDocs {
url: string;
description?: string;
}
const externalDocsKey = createStateSymbol("externalDocs"); const externalDocsKey = createStateSymbol("externalDocs");
/** /**
@ -145,11 +160,18 @@ export const $externalDocs: ExternalDocsDecorator = (
context.program.stateMap(externalDocsKey).set(target, doc); 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 { export function getExternalDocs(program: Program, entity: Type): ExternalDocs | undefined {
return program.stateMap(externalDocsKey).get(entity); return program.stateMap(externalDocsKey).get(entity);
} }
const infoKey = createStateSymbol("info"); const infoKey = createStateSymbol("info");
/** {@inheritdoc InfoDecorator} */
export const $info: InfoDecorator = ( export const $info: InfoDecorator = (
context: DecoratorContext, context: DecoratorContext,
entity: Namespace, entity: Namespace,
@ -166,6 +188,11 @@ export const $info: InfoDecorator = (
setInfo(context.program, entity, data); 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 { export function getInfo(program: Program, entity: Namespace): AdditionalInfo | undefined {
return program.stateMap(infoKey).get(entity); return program.stateMap(infoKey).get(entity);
} }
@ -187,14 +214,3 @@ export function resolveInfo(program: Program, entity: Namespace): AdditionalInfo
function omitUndefined<T extends Record<string, unknown>>(data: T): T { function omitUndefined<T extends Record<string, unknown>>(data: T): T {
return Object.fromEntries(Object.entries(data).filter(([k, v]) => v !== undefined)) as any; 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,
};

Просмотреть файл

@ -64,6 +64,13 @@ export function getOpenAPITypeName(
return name; 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( export function checkDuplicateTypeName(
program: Program, program: Program,
type: Type, type: Type,
@ -114,7 +121,7 @@ export function getParameterKey(
/** /**
* Resolve the OpenAPI operation ID for the given operation using the following logic: * 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 `<operation.name>` * - If operation is defined at the root or under the service namespace return `<operation.name>`
* - Otherwise(operation is under another namespace or interface) return `<namespace/interface.name>_<operation.name>` * - Otherwise(operation is under another namespace or interface) return `<namespace/interface.name>_<operation.name>`
* *

Просмотреть файл

@ -1,3 +1,33 @@
export * from "./decorators.js"; export type {
export * from "./helpers.js"; DefaultResponseDecorator,
export * from "./types.js"; 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";

Просмотреть файл

@ -1,3 +1,7 @@
/**
* Pattern for extension keys.
* In OpenAPI only unknown properties starting with `x-` are allowed.
*/
export type ExtensionKey = `x-${string}`; export type ExtensionKey = `x-${string}`;
/** /**
@ -26,6 +30,9 @@ export interface AdditionalInfo {
license?: License; license?: License;
} }
/**
* Contact information
*/
export interface Contact { export interface Contact {
/** The identifying name of the contact person/organization. */ /** The identifying name of the contact person/organization. */
name?: string; name?: string;
@ -37,6 +44,9 @@ export interface Contact {
email?: string; email?: string;
} }
/**
* License information
*/
export interface License { export interface License {
/** The license name used for the API. */ /** The license name used for the API. */
name: string; 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. */ /** A URL to the license used for the API. MUST be in the format of a URL. */
url?: string; url?: string;
} }
/**
* External Docs info
*/
export interface ExternalDocs {
/** Documentation url */
url: string;
/** Optional description */
description?: string;
}