UNPKG

@azure-tools/typespec-powershell

Version:

An experimental TypeSpec emitter for PowerShell codegen

462 lines (439 loc) 21.5 kB
// Copyright (c) Microsoft Corporation. // Licensed under the MIT License. import { SdkClient, SdkContext, listOperationsInOperationGroup, listOperationGroups } from "@azure-tools/typespec-client-generator-core"; import { HttpOperation, HttpOperationParameter, HttpOperationBody, getHttpOperation } from "@typespec/http"; import { getDoc, getService, ignoreDiagnostics, Program, Model, Type } from "@typespec/compiler"; import { getServers } from "@typespec/http"; import { join } from "path"; import { PwshModel } from "@autorest/powershell"; // import { CodeModel as PwshModel } from "@autorest/codemodel"; import { constantSchemaForApiVersion, getDefaultService, getSchemaForType, schemaCache, stringSchemaForEnum, numberSchemaForEnum, getSchemaForApiVersion, getEnrichedDefaultApiVersion, delayedModelSet } from "../utils/modelUtils.js"; import { Info, Language, Schemas, AllSchemaTypes, SchemaType, ArraySchema, StringSchema, Languages, ObjectSchema } from "@autorest/codemodel"; import { camelCase, deconstruct, pascalCase, serialize } from "@azure-tools/codegen"; import { PSOptions } from "../types/interfaces.js"; import { Request, ImplementationLocation, OperationGroup, Operation, Parameter, Schema, Protocol, Response, HttpHeader } from "@autorest/codemodel"; import { stat } from "fs"; import { extractPagedMetadataNested } from "../utils/operationUtil.js"; import { parseNextLinkName } from "../utils/operationUtil.js"; import { getLroMetadata } from "@azure-tools/typespec-azure-core"; import { getOperationId } from "@typespec/openapi"; const GlobalParameter = "global-parameter"; const urlParameters: Parameter[] = []; let endpoint = '{$host}'; export async function transformPwshModel( client: SdkClient, psContext: SdkContext, emitterOptions: PSOptions ): Promise<PwshModel> { const model = new PwshModel(client.name); getUrlParameters(psContext.program, psContext); model.info = getServiceInfo(psContext.program); model.language.default = getLanguageDefault(psContext.program, emitterOptions); model.operationGroups = getOperationGroups(psContext.program, client, psContext, model, emitterOptions); model.schemas = getSchemas(psContext.program, client, psContext, model); return model; } function handleCircleReference(schemas: Schemas) { for (const schema of schemas.objects || []) { if (schema.properties) { for (const property of schema.properties) { if (property.extensions && property.extensions['circle-ref']) { const refSchema = (schemas.objects || []).filter(s => property.extensions && s.language.default.name === <string>property.extensions['circle-ref']); if (refSchema.length > 0) { property.schema = refSchema[0]; } } } } } return schemas; } function getSchemas(program: Program, client: SdkClient, psContext: SdkContext, model: PwshModel): Schemas { for (const eachModel of delayedModelSet) { getSchemaForType(psContext, eachModel); } const schemas = new Schemas(); const visited = new Set<Schema>; for (const schema of schemaCache.values()) { if (schema.type === SchemaType.Any) { // set name and description for any schema schema.language.default.name = "any"; schema.language.default.description = "Anything"; schemas["any"] = schemas["any"] || []; schemas["any"].push(schema); } else { if (schema.type === SchemaType.Array && (<any>schema).delayType) { (<ArraySchema>schema).elementType = getSchemaForType(psContext, (<any>schema).delayType as Type); (<any>schema).delayType = undefined; } if (!visited.has(schema)) { visited.add(schema); schemas.add(schema); } } } if (stringSchemaForEnum) { schemas.add(stringSchemaForEnum); } if (numberSchemaForEnum) { schemas.add(numberSchemaForEnum); } if (constantSchemaForApiVersion) { schemas.add(constantSchemaForApiVersion); } handleCircleReference(schemas); return schemas; } function getUrlParameters(program: Program, psContext: SdkContext) { const serviceNs = getDefaultService(program)?.type; if (serviceNs) { const host = getServers(program, serviceNs); if (host?.[0]?.url) { endpoint = host[0].url; } if (host && host?.[0] && host?.[0]?.parameters) { for (const key of host[0].parameters.keys()) { const property = host?.[0]?.parameters.get(key); const type = property?.type; if (!property || !type) { continue; } const schema = getSchemaForType(psContext, type); const newParameter = new Parameter(camelCase(deconstruct(key)), getDoc(program, property) || "", schema) newParameter.language.default.serializedName = key; newParameter.protocol.http = newParameter.protocol.http ?? new Protocol(); newParameter.protocol.http.in = "uri"; // ToDo, we need to support x-ms-client is specified. newParameter.implementation = ImplementationLocation.Method; newParameter.required = true; urlParameters.push(newParameter); } } } } function getOperationGroups(program: Program, client: SdkClient, psContext: SdkContext, model: PwshModel, emitterOptions: PSOptions): OperationGroup[] { const operationGroups: OperationGroup[] = []; // list all the operations in the client const clientOperations = listOperationsInOperationGroup(psContext, client); const newGroup = new OperationGroup(""); if (clientOperations.length > 0) { newGroup.language.default.name = newGroup.$key = ""; operationGroups.push(newGroup); } for (const clientOperation of clientOperations) { const op = ignoreDiagnostics(getHttpOperation(program, clientOperation)); if (op.overloads && op.overloads.length > 0) { continue } addOperation(psContext, op, newGroup, model, emitterOptions); // const operationGroup = new OperationGroup(operation.name); // operationGroup.language.default.name = operation.name; // operationGroup.language.default.description = operation.description; // operationGroup.operations.push(operation); // operationGroups.push(operationGroup); } const opGroups = listOperationGroups(psContext, client, true); for (const operationGroup of opGroups) { const newGroup = new OperationGroup(""); newGroup.language.default.name = newGroup.$key = operationGroup.type.name; operationGroups.push(newGroup); const operations = listOperationsInOperationGroup( psContext, operationGroup ); for (const operation of operations) { const op = ignoreDiagnostics(getHttpOperation(program, operation)); // ignore overload base operation if (op.overloads && op.overloads?.length > 0) { continue; } addOperation(psContext, op, newGroup, model, emitterOptions); } } return operationGroups; } function resolveOperationId(psContext: SdkContext, op: HttpOperation, operationGroup: OperationGroup): string { const explicitOperationId = getOperationId(psContext.program, op.operation); if (explicitOperationId) { return explicitOperationId; } return operationGroup.$key + "_" + pascalCase(op.operation.name); } function addOperation(psContext: SdkContext, op: HttpOperation, operationGroup: OperationGroup, model: PwshModel, emitterOptions: PSOptions) { const operationId = resolveOperationId(psContext, op, operationGroup); const newOperation = new Operation( operationId.split('_')[1] ?? pascalCase(op.operation.name), getDoc(psContext.program, op.operation) ?? ""); newOperation.operationId = operationId; // Add Api versions newOperation.apiVersions = newOperation.apiVersions || []; newOperation.apiVersions.push({ version: getEnrichedDefaultApiVersion(psContext.program, psContext) || "" }); // Add URl parameters if (urlParameters.length > 0) { newOperation.parameters = newOperation.parameters || []; newOperation.parameters.push(...urlParameters); } // Add query and path parameters const parameters = op.parameters.parameters.filter(p => p.type === "path" || p.type === "query"); for (const parameter of parameters) { const newParameter = createParameter(psContext, parameter, model); newOperation.parameters = newOperation.parameters || []; newOperation.parameters.push(newParameter); } // Add request newOperation.requests = newOperation.requests || []; const newRequest = new Request(); newOperation.requests.push(newRequest); const headerParameters = op.parameters.parameters.filter(p => p.type === "header"); for (const parameter of headerParameters) { const newParameter = createParameter(psContext, parameter, model); newRequest.parameters = newRequest.parameters || []; newOperation.requests[0].parameters?.push(newParameter); } // Add host parameter if (endpoint === '{$host}') { const hostParameter = createHostParameter(psContext, model); newOperation.parameters = newOperation.parameters || []; newOperation.parameters.push(hostParameter); } // Add request body if it exists if (op.parameters.body && op.parameters.body.bodyKind === "single" && !(op.parameters.body.type.kind === "Intrinsic" && op.parameters.body.type.name === "void")) { const newParameter = createBodyParameter(psContext, op.parameters.body, model); newRequest.parameters = newRequest.parameters || []; newOperation.requests[0].parameters?.push(newParameter); } const httpProtocol = new Protocol(); httpProtocol.method = op.verb; httpProtocol.path = op.path; // hard code the media type to json for the time being by xiaogang. httpProtocol.knownMediaType = "json"; httpProtocol.mediaTypes = ["application/json"]; httpProtocol.uri = endpoint; newOperation.requests[0].protocol.http = httpProtocol; // Add responses include exceptions addResponses(psContext, op, newOperation, model, emitterOptions); // Add extensions addExtensions(psContext, op, newOperation, model); operationGroup.addOperation(newOperation); } function addExtensions(psContext: SdkContext, op: HttpOperation, newOperation: Operation, model: PwshModel) { // Add extensions for pageable const paged = extractPagedMetadataNested(psContext.program, op.responses[0].type as Model); if (paged) { newOperation.extensions = newOperation.extensions || {}; //ToDo: add value if it is specified by xiaogang newOperation.extensions['x-ms-pageable'] = newOperation.extensions['x-ms-pageable'] || {}; newOperation.extensions['x-ms-pageable']['nextLinkName'] = parseNextLinkName(paged) ?? "nextLink"; newOperation.language.default.paging = newOperation.language.default.paging || {}; newOperation.language.default.paging.nextLinkName = parseNextLinkName(paged) ?? "nextLink"; } // Add extensions for long running operation const lro = getLroMetadata(psContext.program, op.operation); if (lro) { newOperation.extensions = newOperation.extensions || {}; newOperation.extensions['x-ms-long-running-operation'] = true; newOperation.extensions['x-ms-long-running-operation-options'] = newOperation.extensions['x-ms-long-running-operation-options'] || {}; newOperation.extensions['x-ms-long-running-operation-options']['final-state-via'] = lro.finalStateVia; } } function addResponses(psContext: SdkContext, op: HttpOperation, newOperation: Operation, model: PwshModel, emitterOptions: PSOptions) { const responses = op.responses; newOperation.responses = newOperation.responses || []; newOperation.exceptions = newOperation.exceptions || []; if (responses) { for (const response of responses) { const newResponse = new Response(); // newOperation.responses[response.statusCode] || newOperation.responses.default; // if (!newResponse) { // newOperation.responses[response.statusCode] = newResponse; // } newResponse.language.default.name = ''; newResponse.language.default.description = response.description || ""; const statusCode = response.statusCodes.toString(); // Add schema for response body if (response.responses[0].body) { const schema = getSchemaForType(psContext, response.responses[0].body.type); (<any>newResponse).schema = schema; } // Add headers // we merge headers here, if the same header is defined in multiple responses, we only add it once. // This is aligned with the behavior of typescript emitter and typespec-autorest emitter. newResponse.protocol.http = newResponse.protocol.http ?? new Protocol(); const lroHeaders = ["location", "retry-after", "azure-asyncoperation"]; const addedKeys: string[] = []; for (const innerResponse of response.responses) { if (innerResponse.headers) { for (const key in innerResponse.headers) { if (addedKeys.includes(key) || (emitterOptions["remove-lro-headers"] && lroHeaders.includes(key.toLowerCase()))) { continue; } else { addedKeys.push(key); } newResponse.protocol.http.headers = newResponse.protocol.http.headers || []; const header = innerResponse.headers[key]; const headerSchema = getSchemaForType(psContext, header.type); const headerResponse = new HttpHeader(key, headerSchema); headerResponse.language = new Languages(); headerResponse.language.default = new Language(); headerResponse.language.default.description = getDoc(psContext.program, header) || ""; headerResponse.language.default.name = pascalCase(deconstruct(key)); newResponse.protocol.http.headers.push(headerResponse); } } } newResponse.protocol.http.statusCodes = statusCode === "*" ? ["default"] : [statusCode]; newResponse.protocol.http.knownMediaType = "json"; newResponse.protocol.http.mediaTypes = ["application/json"]; if (statusCode.startsWith("2")) { newOperation.responses.push(newResponse); } else { newOperation.exceptions.push(newResponse); } } } } function createBodyParameter(psContext: SdkContext, parameter: HttpOperationBody, model: PwshModel): Parameter { const paramSchema = parameter.property?.sourceProperty ? getSchemaForType(psContext, parameter.property?.sourceProperty?.type) : getSchemaForType(psContext, parameter.type) const newParameter = new Parameter(parameter.property?.name || "", parameter.property ? getDoc(psContext.program, parameter.property) || "" : "", paramSchema); newParameter.protocol.http = newParameter.protocol.http ?? new Protocol(); newParameter.protocol.http.in = "body"; // ToDo, we need to support x-ms-client is specified. newParameter.implementation = ImplementationLocation.Method; newParameter.required = !(parameter.property && parameter.property.optional); return newParameter; } function createHostParameter(psContext: SdkContext, model: PwshModel): Parameter { const matchParameters = (model.globalParameters || []).filter(p => p.language.default.serializedName === '$host'); if (matchParameters.length > 0) { return matchParameters[0]; } else { const newParameter = new Parameter('$host', "server parameter", new StringSchema("", "")); //getSchemaForType(psContext, "string") newParameter.language.default.serializedName = '$host'; newParameter.clientDefaultValue = "https://management.azure.com"; newParameter.protocol.http = newParameter.protocol.http ?? new Protocol(); newParameter.protocol.http.in = "uri"; newParameter.implementation = ImplementationLocation.Client; newParameter.required = true; newParameter.extensions = newParameter.extensions || {}; newParameter.extensions["x-ms-skip-url-encoding"] = true; model.globalParameters = model.globalParameters || []; model.globalParameters.push(newParameter); return newParameter; } } function createParameter(psContext: SdkContext, parameter: HttpOperationParameter, model: PwshModel): Parameter { if (parameter.type === "query" && parameter.name === "api-version" || parameter.type === "path" && parameter.name === "subscriptionId") { const matchParameters = (model.globalParameters || []).filter(p => p.language.default.serializedName === parameter.name); if (matchParameters.length > 0) { return matchParameters[0]; } else { const paramSchema = parameter.name === "api-version" ? getSchemaForApiVersion(psContext, parameter.param.type) : (parameter.param.sourceProperty ? getSchemaForType(psContext, parameter.param.sourceProperty) : getSchemaForType(psContext, parameter.param)); const newParameter = new Parameter(pascalCase(deconstruct(parameter.name)), getDoc(psContext.program, parameter.param) || "", paramSchema); if (newParameter.language.default.name === "ApiVersion") { //to align with modelerfour newParameter.language.default.name = "apiVersion"; } newParameter.language.default.serializedName = parameter.name; newParameter.protocol.http = newParameter.protocol.http ?? new Protocol(); newParameter.protocol.http.in = parameter.type; newParameter.implementation = ImplementationLocation.Client; newParameter.required = !parameter.param.optional; model.globalParameters = model.globalParameters || []; model.globalParameters.push(newParameter); return newParameter; } } else { // always create the parameter const paramSchema = parameter.param.sourceProperty ? getSchemaForType(psContext, parameter.param.sourceProperty) : getSchemaForType(psContext, parameter.param); const newParameter = new Parameter(parameter.name, getDoc(psContext.program, parameter.param) || "", paramSchema); newParameter.language.default.serializedName = parameter.name; newParameter.protocol.http = newParameter.protocol.http ?? new Protocol(); newParameter.protocol.http.in = parameter.type; if ((<any>parameter).allowReserved) { newParameter.extensions = newParameter.extensions || {}; newParameter.extensions["x-ms-skip-url-encoding"] = true; } // ToDo, we need to support x-ms-client is specified. newParameter.implementation = ImplementationLocation.Method; newParameter.required = !parameter.param.optional; return newParameter; } } // function addGlobalParameter(parameter: Parameter, model: PwshModel) { // if ((parameter.protocol?.http?.in == "query" && parameter.language.default.name === "api-version") // || (parameter.protocol?.http?.in == "path" && parameter.language.default.name === "subscriptionId")) { // model.globalParameters = model.globalParameters || []; // model.globalParameters.push(parameter); // } // } function getServiceInfo(program: Program): Info { const defaultService = getDefaultService(program); const info = new Info(defaultService?.title || ""); info.description = defaultService && getDoc(program, defaultService.type); return info; } function getLanguageDefault(program: Program, emitterOptions: PSOptions): Language { const defaultLanguage: Language = { name: emitterOptions.packageDetails?.name ?? // Todo: may need to normalize the name pascalCase(deconstruct(emitterOptions?.title ?? getDefaultService(program)?.title ?? "")), description: '' }; return defaultLanguage; } // export function transformUrlInfo(dpgContext: SdkContext): UrlInfo | undefined { // const program = dpgContext.program; // const serviceNs = getDefaultService(program)?.type; // let endpoint = undefined; // const urlParameters: PathParameter[] = []; // if (serviceNs) { // const host = getServers(program, serviceNs); // if (host?.[0]?.url) { // endpoint = host[0].url; // } // if (host && host?.[0] && host?.[0]?.parameters) { // // Currently we only support one parameter in the servers definition // for (const key of host[0].parameters.keys()) { // const property = host?.[0]?.parameters.get(key); // const type = property?.type; // if (!property || !type) { // continue; // } // const schema = getSchemaForType(dpgContext, type, { // usage: [SchemaContext.Exception, SchemaContext.Input], // needRef: false, // relevantProperty: property // }); // urlParameters.push({ // oriName: key, // name: normalizeName(key, NameType.Parameter, true), // type: getTypeName(schema), // description: // (getDoc(program, property) && // getFormattedPropertyDoc(program, property, schema, " ")) ?? // getFormattedPropertyDoc(program, type, schema, " " /* sperator*/), // value: predictDefaultValue(dpgContext, host?.[0]?.parameters.get(key)) // }); // } // } // } // if (endpoint && urlParameters.length > 0) { // for (const param of urlParameters) { // if (param.oriName) { // const regexp = new RegExp(`{${param.oriName}}`, "g"); // endpoint = endpoint.replace(regexp, `{${param.name}}`); // } // } // } // // Set the default value if missing endpoint parameter // if (endpoint == undefined && urlParameters.length === 0) { // endpoint = "{endpoint}"; // urlParameters.push({ // name: "endpoint", // type: "string" // }); // } // return { endpoint, urlParameters }; // }