UNPKG

@typespec/http-server-js

Version:

TypeSpec HTTP server code generator for JavaScript

268 lines (228 loc) 8.19 kB
// Copyright (c) Microsoft Corporation // Licensed under the MIT license. import { Interface, Operation, Type, UnionVariant, isErrorModel } from "@typespec/compiler"; import { JsContext, Module, PathCursor } from "../ctx.js"; import { canonicalizeHttpOperation } from "../http/operation.js"; import { parseCase } from "../util/case.js"; import { getAllProperties } from "../util/extends.js"; import { bifilter, indent } from "../util/iter.js"; import { keywordSafe } from "../util/keywords.js"; import { emitDocumentation } from "./documentation.js"; import { emitTypeReference, isValueLiteralType } from "./reference.js"; import { emitUnionType } from "./union.js"; /** * Emit an interface declaration. * * @param ctx - The emitter context. * @param iface - The interface to emit. * @param module - The module that this interface is written into. */ export function* emitInterface(ctx: JsContext, iface: Interface, module: Module): Iterable<string> { const name = parseCase(iface.name).pascalCase; yield* emitDocumentation(ctx, iface); yield `export interface ${name}<Context = unknown> {`; yield* indent(emitOperationGroup(ctx, iface.operations.values(), module)); yield "}"; yield ""; } /** * Emit a list of operation signatures. * * @param ctx - The emitter context. * @param operations - The operations to emit. * @param module - The module that the operations are written into. */ export function* emitOperationGroup( ctx: JsContext, operations: Iterable<Operation>, module: Module, ): Iterable<string> { for (const op of operations) { yield* emitOperation(ctx, op, module); yield ""; } } /** * Emit a single operation signature. * * @param ctx - The emitter context. * @param op - The operation to emit. * @param module - The module that the operation is written into. */ export function* emitOperation(ctx: JsContext, op: Operation, module: Module): Iterable<string> { op = canonicalizeHttpOperation(ctx, op); const opNameCase = parseCase(op.name); const opName = opNameCase.camelCase; const allParameters = getAllProperties(op.parameters); const hasOptions = allParameters.some((p) => p.optional); const returnTypeReference = emitTypeReference(ctx, op.returnType, op, module, { altName: opNameCase.pascalCase + "Result", }); const returnType = `Promise<${returnTypeReference}>`; const params: string[] = []; for (const param of allParameters) { // If the type is a value literal, then we consider it a _setting_ and not a parameter. // This allows us to exclude metadata parameters (such as contentType) from the generated interface. if (param.optional || isValueLiteralType(param.type)) continue; const paramNameCase = parseCase(param.name); const paramName = keywordSafe(paramNameCase.camelCase); const outputTypeReference = emitTypeReference(ctx, param.type, param, module, { altName: opNameCase.pascalCase + paramNameCase.pascalCase, }); params.push(`${paramName}: ${outputTypeReference}`); } const paramsDeclarationLine = params.join(", "); yield* emitDocumentation(ctx, op); if (hasOptions) { const optionsTypeName = opNameCase.pascalCase + "Options"; emitOptionsType(ctx, op, module, optionsTypeName); const paramsFragment = params.length > 0 ? `${paramsDeclarationLine}, ` : ""; // prettier-ignore yield `${opName}(ctx: Context, ${paramsFragment}options?: ${optionsTypeName}): ${returnType};`; yield ""; } else { // prettier-ignore yield `${opName}(ctx: Context, ${paramsDeclarationLine}): ${returnType};`; yield ""; } } /** * Emit a declaration for an options type including the optional parameters of an operation. * * @param ctx - The emitter context. * @param operation - The operation to emit the options type for. * @param module - The module that the options type is written into. * @param optionsTypeName - The name of the options type. */ export function emitOptionsType( ctx: JsContext, operation: Operation, module: Module, optionsTypeName: string, ) { module.imports.push({ binder: [optionsTypeName], from: ctx.syntheticModule, }); const options = [...operation.parameters.properties.values()].filter((p) => p.optional); ctx.syntheticModule.declarations.push([ `export interface ${optionsTypeName} {`, ...options.flatMap((p) => [ ` ${keywordSafe(parseCase(p.name).camelCase)}?: ${emitTypeReference(ctx, p.type, p, module, { altName: optionsTypeName + parseCase(p.name).pascalCase, })};`, ]), "}", "", ]); } export interface SplitReturnTypeCommon { typeReference: string; target: Type | [PathCursor, string] | undefined; } export interface OrdinarySplitReturnType extends SplitReturnTypeCommon { kind: "ordinary"; } export interface UnionSplitReturnType extends SplitReturnTypeCommon { kind: "union"; variants: UnionVariant[]; } export type SplitReturnType = OrdinarySplitReturnType | UnionSplitReturnType; const DEFAULT_NO_VARIANT_RETURN_TYPE = "never"; const DEFAULT_NO_VARIANT_SPLIT: SplitReturnType = { kind: "ordinary", typeReference: DEFAULT_NO_VARIANT_RETURN_TYPE, target: undefined, }; export function isInfallible(split: SplitReturnType): boolean { return ( (split.kind === "ordinary" && split.typeReference === "never") || (split.kind === "union" && split.variants.length === 0) ); } export function splitReturnType( ctx: JsContext, type: Type, module: Module, altBaseName: string, ): [SplitReturnType, SplitReturnType] { const successAltName = altBaseName + "Response"; const errorAltName = altBaseName + "ErrorResponse"; if (type.kind === "Union") { const [successVariants, errorVariants] = bifilter( type.variants.values(), (v) => !isErrorModel(ctx.program, v.type), ); const successTypeReference = successVariants.length === 0 ? DEFAULT_NO_VARIANT_RETURN_TYPE : successVariants.length === 1 ? emitTypeReference(ctx, successVariants[0].type, successVariants[0], module, { altName: successAltName, }) : emitUnionType(ctx, successVariants, module); const errorTypeReference = errorVariants.length === 0 ? DEFAULT_NO_VARIANT_RETURN_TYPE : errorVariants.length === 1 ? emitTypeReference(ctx, errorVariants[0].type, errorVariants[0], module, { altName: errorAltName, }) : emitUnionType(ctx, errorVariants, module); const successSplit: SplitReturnType = successVariants.length > 1 ? { kind: "union", variants: successVariants, typeReference: successTypeReference, target: undefined, } : { kind: "ordinary", typeReference: successTypeReference, target: successVariants[0]?.type, }; const errorSplit: SplitReturnType = errorVariants.length > 1 ? { kind: "union", variants: errorVariants, typeReference: errorTypeReference, // target: module.cursor.resolveRelativeItemPath(errorTypeReference), target: undefined, } : { kind: "ordinary", typeReference: errorTypeReference, target: errorVariants[0]?.type, }; return [successSplit, errorSplit]; } else { // No splitting, just figure out if the type is an error type or not and make the other infallible. if (isErrorModel(ctx.program, type)) { const typeReference = emitTypeReference(ctx, type, type, module, { altName: altBaseName + "ErrorResponse", }); return [ DEFAULT_NO_VARIANT_SPLIT, { kind: "ordinary", typeReference, target: type, }, ]; } else { const typeReference = emitTypeReference(ctx, type, type, module, { altName: altBaseName + "SuccessResponse", }); return [ { kind: "ordinary", typeReference, target: type, }, DEFAULT_NO_VARIANT_SPLIT, ]; } } }