UNPKG

@azure-tools/typespec-powershell

Version:

An experimental TypeSpec emitter for PowerShell codegen

624 lines (577 loc) 18 kB
// Copyright (c) Microsoft Corporation. // Licensed under the MIT License. // import { // NameType, // Paths, // ResponseMetadata, // ResponseTypes, // getLroLogicalResponseName, // getResponseTypeName, // normalizeName // } from "@azure-tools/rlc-common"; import { ignoreDiagnostics, isList, Model, ModelProperty, Operation, Program, Type } from "@typespec/compiler"; import { HttpOperation, HttpOperationParameter, HttpOperationResponse, HttpStatusCodesEntry, getHttpOperation } from "@typespec/http"; import { getLroMetadata, getPagedResult, PagedResultMetadata } from "@azure-tools/typespec-azure-core"; import { getWireName, SdkClient, listOperationGroups, listOperationsInOperationGroup, SdkContext } from "@azure-tools/typespec-client-generator-core"; import { listOperations } from "./clientUtils.js"; import { $ } from "@typespec/compiler/typekit"; // import { // OperationLroDetail, // OPERATION_LRO_LOW_PRIORITY, // OPERATION_LRO_HIGH_PRIORITY // } from "@azure-tools/rlc-common"; // import { isByteOrByteUnion } from "./modelUtils.js"; // import { SdkContext } from "./interfaces.js"; // import { getOperationNamespaceInterfaceName } from "./namespaceUtils.js"; // import { KnownMediaType, knownMediaType } from "./mediaTypes.js"; // Sorts the responses by status code export function sortedOperationResponses(responses: HttpOperationResponse[]) { return responses.sort((a, b) => { if (a.statusCodes === "*") { return 1; } if (b.statusCodes === "*") { return -1; } const aStatus = typeof a.statusCodes === "number" ? a.statusCodes : a.statusCodes.start; const bStatus = typeof b.statusCodes === "number" ? b.statusCodes : b.statusCodes.start; return aStatus - bStatus; }); } /** * This function computes all the response types error and success * an operation can end up returning. */ // ToDo: get response by xiaogang // export function getOperationResponseTypes( // dpgContext: SdkContext, // operation: HttpOperation // ): ResponseTypes { // const returnTypes: ResponseTypes = { // error: [], // success: [] // }; // function getResponseType(responses: HttpOperationResponse[]) { // return responses.map((r) => { // const statusCode = getOperationStatuscode(r); // const responseName = getResponseTypeName( // getOperationGroupName(dpgContext, operation), // getOperationName(dpgContext.program, operation.operation), // statusCode // ); // return responseName; // }); // } // if (operation.responses && operation.responses.length) { // returnTypes.error = getResponseType( // operation.responses.filter((r) => isDefaultStatusCode(r.statusCodes)) // ); // returnTypes.success = getResponseType( // operation.responses.filter((r) => isDefinedStatusCode(r.statusCodes)) // ); // } // return returnTypes; // } /** * Extracts all success or defined status codes for a give operation */ export function getOperationSuccessStatus(operation: HttpOperation): string[] { const responses = operation.responses ?? []; const status: string[] = []; for (const response of responses) { if (isDefinedStatusCode(response.statusCodes)) { status.push(getOperationStatuscode(response)); } } return status; } export function getOperationStatuscode( response: HttpOperationResponse ): string { const statusCodes = response.statusCodes; if (statusCodes === "*") { return "default"; } else if (typeof statusCodes === "number") { return String(statusCodes); } else { // FIXME - this is a hack to get the first status code // https://github.com/Azure/autorest.typescript/issues/2063 return String(statusCodes.start); } } // export function getOperationGroupName( // dpgContext: SdkContext, // route?: HttpOperation // ): string; // export function getOperationGroupName( // dpgContext: SdkContext, // operation?: Operation // ): string; // export function getOperationGroupName( // dpgContext: SdkContext, // operationOrRoute?: Operation | HttpOperation // ) { // if (!dpgContext.rlcOptions?.enableOperationGroup || !operationOrRoute) { // return ""; // } // // If this is a HttpOperation // if ((operationOrRoute as any).kind !== "Operation") { // operationOrRoute = (operationOrRoute as HttpOperation).operation; // } // const operation = operationOrRoute as Operation; // const namespaceNames = getOperationNamespaceInterfaceName( // dpgContext, // operation // ); // return namespaceNames // .map((name) => { // return normalizeName(name, NameType.Interface, true); // }) // .join(""); // } // export function getOperationName(program: Program, operation: Operation) { // const projectedOperationName = getProjectedName(program, operation, "json"); // return normalizeName( // projectedOperationName ?? operation.name, // NameType.Interface, // true // ); // } export function isDefaultStatusCode(statusCodes: HttpStatusCodesEntry) { return statusCodes === "*"; } export function isDefinedStatusCode(statusCodes: HttpStatusCodesEntry) { return statusCodes !== "*"; } // ToDo: Binary playload by xiaogang // export function isBinaryPayload( // dpgContext: SdkContext, // body: Type, // contentType: string | string[] // ) { // const knownMediaTypes: KnownMediaType[] = ( // Array.isArray(contentType) ? contentType : [contentType] // ).map((ct) => knownMediaType(ct)); // for (const type of knownMediaTypes) { // if (type === KnownMediaType.Binary && isByteOrByteUnion(dpgContext, body)) { // return true; // } // } // return false; // } export function isLongRunningOperation( program: Program, operation: HttpOperation ) { return Boolean(getLroMetadata(program, operation.operation)); } /** * Return if we have a client-level LRO overloading * @param pathDictionary * @returns */ // export function getClientLroOverload(pathDictionary: Paths) { // let lroCounts = 0, // allowCounts = 0; // for (const details of Object.values(pathDictionary)) { // for (const methodDetails of Object.values(details.methods)) { // const lroDetail = methodDetails[0].operationHelperDetail?.lroDetails; // if (lroDetail?.isLongRunning) { // lroCounts++; // if (!lroDetail.operationLroOverload) { // return false; // } // allowCounts++; // } // } // } // return Boolean(lroCounts > 0 && lroCounts === allowCounts); // } /** * Check if we have an operation-level overloading * @param program * @param operation The operation detail * @param existingResponseTypes auxilary param for current response types * @param existingResponses auxilary param for raw response data * @returns */ // export function getOperationLroOverload( // program: Program, // operation: HttpOperation, // existingResponseTypes?: ResponseTypes, // existingResponses?: ResponseMetadata[] // ) { // const metadata = getLroMetadata(program, operation.operation); // if (!metadata) { // return false; // } // const hasSuccessReturn = existingResponses?.filter((r) => // r.statusCode.startsWith("20") // ); // if (existingResponseTypes?.success || hasSuccessReturn) { // return true; // } // return false; // } /** * Extract the operation LRO details * @param program * @param operation Operation detail * @param responsesTypes Calculated response types * @param operationGroupName Operation group name * @returns */ // export function extractOperationLroDetail( // program: Program, // operation: HttpOperation, // responsesTypes: ResponseTypes, // operationGroupName: string // ): OperationLroDetail { // let logicalResponseTypes: ResponseTypes | undefined; // let precedence = OPERATION_LRO_LOW_PRIORITY; // const operationLroOverload = getOperationLroOverload( // program, // operation, // responsesTypes // ); // if (operationLroOverload) { // logicalResponseTypes = { // error: responsesTypes.error, // success: [ // getLroLogicalResponseName( // operationGroupName, // getOperationName(program, operation.operation) // ) // ] // }; // const metadata = getLroMetadata(program, operation.operation); // precedence = // metadata?.finalStep && // metadata.finalStep.kind === "pollingSuccessProperty" && // metadata?.finalStep.target && // metadata?.finalStep?.target?.name === "result" // ? OPERATION_LRO_HIGH_PRIORITY // : OPERATION_LRO_LOW_PRIORITY; // } // return { // isLongRunning: Boolean(getLroMetadata(program, operation.operation)), // logicalResponseTypes, // operationLroOverload, // precedence // }; // } export function hasPollingOperations( program: Program, client: SdkClient, dpgContext: SdkContext ) { const clientOperations = listOperations(client); for (const clientOp of clientOperations) { const route = ignoreDiagnostics(getHttpOperation(program, clientOp)); // ignore overload base operation if (route.overloads && route.overloads?.length > 0) { continue; } if (isLongRunningOperation(program, route)) { return true; } } return false; } export function isPagingOperation(program: Program, operation: HttpOperation) { return extractPageDetails(program, operation) !== undefined; } function mapFirstSegmentForResultSegments( resultSegments: ModelProperty[] | undefined, responses: HttpOperationResponse[] ): ModelProperty[] | undefined { const pagingBodyType = responses.find((r) => r.statusCodes === 200) ?.responses[0]?.body; if (!pagingBodyType || pagingBodyType.bodyKind !== "single") return undefined; const bodyType = pagingBodyType.type; if (resultSegments === undefined || bodyType === undefined) return undefined; // TCGC use Http response type as the return type // For implicit body response, we need to locate the first segment in the response type // Several cases: // 1. `op test(): {items, nextLink}` // 2. `op test(): {items, nextLink} & {a, b, c}` // 3. `op test(): {@bodyRoot body: {items, nextLink}}` if (resultSegments.length > 0 && bodyType && bodyType.kind === "Model") { for (let i = 0; i < resultSegments.length; i++) { const segment = resultSegments[i]; for (const property of bodyType.properties ?? []) { if ( property && segment && findRootSourceProperty(property[1]) === findRootSourceProperty(segment) ) { return [property[1], ...resultSegments.slice(i + 1)]; } } } } return resultSegments; } function findRootSourceProperty(property: ModelProperty): ModelProperty { while (property.sourceProperty) { property = property.sourceProperty; } return property; } export function hasPagingOperations( program: Program, client: SdkClient, dpgContext: SdkContext ) { const clientOperations = listOperationsInOperationGroup(dpgContext, client); for (const clientOp of clientOperations) { const route = ignoreDiagnostics(getHttpOperation(program, clientOp)); // ignore overload base operation if (route.overloads && route.overloads?.length > 0) { continue; } if (isPagingOperation(program, route)) { return true; } } const operationGroups = listOperationGroups(dpgContext, client, true); for (const operationGroup of operationGroups) { const operations = listOperationsInOperationGroup( dpgContext, operationGroup ); for (const op of operations) { const route = ignoreDiagnostics(getHttpOperation(program, op)); // ignore overload base operation if (route.overloads && route.overloads?.length > 0) { continue; } if (isPagingOperation(program, route)) { return true; } } } return false; } export function extractPagedMetadataNested( program: Program, type: Model ): PagedResultMetadata | undefined { // This only works for `is Page<T>` not `extends Page<T>`. let paged = getPagedResult(program, type); if (paged) { return paged; } if (type.baseModel) { paged = getPagedResult(program, type.baseModel); } if (paged) { return paged; } const templateArguments = type.templateMapper?.args; if (templateArguments) { for (const argument of templateArguments) { const modelArgument = argument as Model; if (modelArgument) { paged = extractPagedMetadataNested(program, modelArgument); if (paged) { return paged; } } } } return paged; } export function getSpecialSerializeInfo( paramType: string, paramFormat: string ) { const hasMultiCollection = getHasMultiCollection(paramType, paramFormat); const hasPipeCollection = getHasPipeCollection(paramType, paramFormat); const hasSsvCollection = getHasSsvCollection(paramType, paramFormat); const hasTsvCollection = getHasTsvCollection(paramType, paramFormat); const hasCsvCollection = getHasCsvCollection(paramType, paramFormat); const descriptions = []; const collectionInfo = []; if (hasMultiCollection) { descriptions.push("buildMultiCollection"); collectionInfo.push("multi"); } if (hasSsvCollection) { descriptions.push("buildSsvCollection"); collectionInfo.push("ssv"); } if (hasTsvCollection) { descriptions.push("buildTsvCollection"); collectionInfo.push("tsv"); } if (hasPipeCollection) { descriptions.push("buildPipeCollection"); collectionInfo.push("pipe"); } if (hasCsvCollection) { descriptions.push("buildCsvCollection"); collectionInfo.push("csv"); } return { hasMultiCollection, hasPipeCollection, hasSsvCollection, hasTsvCollection, hasCsvCollection, descriptions, collectionInfo }; } function getHasMultiCollection(paramType: string, paramFormat: string) { return ( (paramType === "query" || paramType === "header") && paramFormat === "multi" ); } function getHasSsvCollection(paramType: string, paramFormat: string) { return paramType === "query" && paramFormat === "ssv"; } function getHasTsvCollection(paramType: string, paramFormat: string) { return paramType === "query" && paramFormat === "tsv"; } function getHasCsvCollection(paramType: string, paramFormat: string) { return paramType === "header" && paramFormat === "csv"; } function getHasPipeCollection(paramType: string, paramFormat: string) { return paramType === "query" && paramFormat === "pipes"; } export function hasCollectionFormatInfo( paramType: string, paramFormat: string ) { return ( getHasMultiCollection(paramType, paramFormat) || getHasSsvCollection(paramType, paramFormat) || getHasTsvCollection(paramType, paramFormat) || getHasCsvCollection(paramType, paramFormat) || getHasPipeCollection(paramType, paramFormat) ); } export function getCollectionFormatHelper( paramType: string, paramFormat: string ) { const detail = getSpecialSerializeInfo(paramType, paramFormat); return detail.descriptions.length > 0 ? detail.descriptions[0] : undefined; } export function getCustomRequestHeaderNameForOperation( route: HttpOperation ): string | undefined { const params = route.parameters.parameters.filter( isCustomClientRequestIdParam ); if (params.length > 0) { return "client-request-id"; } return undefined; } export function isCustomClientRequestIdParam(param: HttpOperationParameter) { return ( param.type === "header" && param.name.toLowerCase() === "client-request-id" ); } export function isIgnoredHeaderParam(param: HttpOperationParameter) { return ( isCustomClientRequestIdParam(param) || (param.type === "header" && ["return-client-request-id", "ocp-date"].includes( param.name.toLowerCase() )) ); } export function parseNextLinkName( paged: PagedResultMetadata ): string | undefined { return paged.nextLinkProperty?.name; } export function parseItemName(paged: PagedResultMetadata): string | undefined { // TODO: support the nested item names return (paged.itemsSegments ?? [])[0]; } export interface PageDetails { nextLinkNames: string[]; itemNames: string[]; } export function extractPageDetails( program: Program, operation: HttpOperation ): PageDetails | undefined { if (isList(program, operation.operation)) { // If the operation is a list, we don't need to extract paging details. const metadata = $(program).operation.getPagingMetadata( operation.operation ); if (metadata === undefined) { // would fallback to default paging metadata return undefined; } const nextLinkPath = mapFirstSegmentForResultSegments( metadata?.output.nextLink?.path, operation.responses ); const itemNamePath = mapFirstSegmentForResultSegments( metadata?.output.pageItems?.path, operation.responses ); if ( (nextLinkPath && nextLinkPath?.length > 1) || (itemNamePath && itemNamePath?.length > 1) ) { return undefined; } const nextLinkNames = nextLinkPath?.map((prop) => prop.name).join(".") ?? "nextLink"; const itemNames = itemNamePath?.map((prop) => prop.name).join(".") ?? "value"; return { nextLinkNames: [nextLinkNames], itemNames: [itemNames] }; } else { // TODO: remember to remove this once Azure Paging is removed. for (const response of operation.responses) { const paged = extractPagedMetadataNested(program, response.type as Model); if (paged) { const nextLinkName = parseNextLinkName(paged) ?? "nextLink"; const itemName = parseItemName(paged) ?? "value"; return { nextLinkNames: [nextLinkName], itemNames: [itemName] }; } } } return undefined; }