UNPKG

@autorest/go

Version:
616 lines 29.9 kB
/*--------------------------------------------------------------------------------------------- * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ /* eslint-disable @typescript-eslint/no-unsafe-argument */ /* eslint-disable @typescript-eslint/no-unsafe-assignment */ /* eslint-disable @typescript-eslint/no-unsafe-member-access */ import * as m4 from '@autorest/codemodel'; import { KnownMediaType } from '@azure-tools/codegen'; import { values } from '@azure-tools/linq'; import { adaptXMLInfo } from './types.js'; import { adaptWireType, hasDescription } from './types.js'; import * as go from '../../../codemodel.go/src/index.js'; import { ensureNameCase } from '../../../naming.go/src/index.js'; import * as helpers from '../transform/helpers.js'; // track all of the client and parameter group params across all operations // as not every option might contain them, and parameter groups can be shared // across multiple operations const clientParams = new Map(); const paramGroups = new Map(); export function adaptClients(m4CodeModel, codeModel) { // since we only support single packages in autorest, the root will // either be the package in the containing module or the module itself. const pkg = codeModel.root.kind === 'containingModule' ? codeModel.root.package : codeModel.root; for (const group of values(m4CodeModel.operationGroups)) { const client = adaptClient(group, pkg); for (const op of values(group.operations)) { const httpPath = op.requests[0].protocol.http.path; const httpMethod = op.requests[0].protocol.http.method; let method; const naming = adaptMethodNaming(op); if (helpers.isLROOperation(op) && helpers.isPageableOperation(op)) { method = new go.LROPageableMethod(op.language.go.name, client, httpPath, httpMethod, getStatusCodes(op), naming); method.finalStateVia = op.extensions?.['x-ms-long-running-operation-options']?.['final-state-via']; } else if (helpers.isLROOperation(op)) { method = new go.LROMethod(op.language.go.name, client, httpPath, httpMethod, getStatusCodes(op), naming); method.finalStateVia = op.extensions?.['x-ms-long-running-operation-options']?.['final-state-via']; } else if (helpers.isPageableOperation(op)) { if (op.language.go.paging.isNextOp) { continue; } method = new go.PageableMethod(op.language.go.name, client, httpPath, httpMethod, getStatusCodes(op), naming); } else { method = new go.SyncMethod(op.language.go.name, client, httpPath, httpMethod, getStatusCodes(op), naming); } populateMethod(op, m4CodeModel, method); client.methods.push(method); } // if any client parameters were adapted, add them to the client if (group.language.go.clientParams) { for (const param of group.language.go.clientParams) { const adaptedParam = clientParams.get(`${client.name}-${param.language.go.name}`); if (!adaptedParam) { throw new Error(`missing adapted client parameter ${param.language.go.name}`); } client.parameters.push(adaptedParam); } } if (codeModel.type === 'azure-arm') { const ctor = new go.Constructor(pkg, `New${client.name}`); // add any modeled parameter first, which should only be the subscriptionID, then add TokenCredential for (const param of client.parameters) { ctor.parameters.push(param); } client.instance = new go.Constructable(go.newClientOptions(pkg, codeModel.type, group.language.go.clientName)); // we don't need the scopes for ARM, it's handled by pipeline policy ctor.parameters.push(new go.ClientCredentialParameter('credential', new go.TokenCredential([]))); client.instance.constructors.push(ctor); } pkg.clients.push(client); } } // used to ensure unique instances of go.NextPageMethod since they can be shared across operations // only adaptNextPageMethod should touch this! const adaptedNextPageMethods = new Map(); function adaptNextPageMethod(op, m4CodeModel, client) { const nextPageMethodName = op.language.go.paging.nextLinkOperation.language.go.name; let nextPageMethod = adaptedNextPageMethods.get(nextPageMethodName); if (!nextPageMethod) { const httpPath = op.language.go.paging.nextLinkOperation.requests[0].protocol.http.path; const httpMethod = op.language.go.paging.nextLinkOperation.requests[0].protocol.http.method; nextPageMethod = new go.NextPageMethod(nextPageMethodName, client, httpPath, httpMethod, getStatusCodes(op.language.go.paging.nextLinkOperation)); populateMethod(op.language.go.paging.nextLinkOperation, m4CodeModel, nextPageMethod); adaptedNextPageMethods.set(nextPageMethodName, nextPageMethod); } return nextPageMethod; } function populateMethod(op, m4CodeModel, method) { const getNextLinkStrategy = function (method, nextLinkName) { // find the next link field in the response envelope's type if (method.returns.result?.kind === 'modelResult') { for (const field of method.returns.result.modelType.fields) { if (field.name === nextLinkName) { return new go.PageableStrategyNextLink([field]); } } throw new Error(`didn't find next link field ${nextLinkName} in type ${method.returns.result.modelType.name} for method ${method.name}`); } else { throw new Error(`unexpected method result kind ${method.returns.result?.kind}`); } }; if (method.kind !== 'nextPageMethod') { if (hasDescription(op.language.go)) { method.docs.description = op.language.go.description; } let optionalParamsGroup = paramGroups.get(op.language.go.optionalParamGroup.schema.language.go.name); if (!optionalParamsGroup) { optionalParamsGroup = adaptParameterGroup(op.language.go.optionalParamGroup, method.receiver.type.pkg, 'method'); paramGroups.set(op.language.go.optionalParamGroup.schema.language.go.name, optionalParamsGroup); } method.optionalParamsGroup = optionalParamsGroup; method.returns = adaptResponseEnvelope(m4CodeModel, op, method.receiver.type.pkg, method); if (method.kind === 'lroPageableMethod' || method.kind === 'pageableMethod') { if (op.language.go.paging.nextLinkName) { method.strategy = getNextLinkStrategy(method, op.language.go.paging.nextLinkName); if (op.language.go.paging.nextLinkOperation) { // adapt the next link operation method.strategy.method = adaptNextPageMethod(op, m4CodeModel, method.receiver.type); } } } } adaptMethodParameters(op, method); } function adaptHeaderScalarType(schema, forParam, pkg) { // for header params, we never pass the element type by pointer const type = adaptWireType(schema, pkg, forParam); if (go.isHeaderScalarType(type)) { return type; } throw new Error(`unexpected header scalar parameter type ${schema.type}`); } function adaptPathScalarParameterType(schema, pkg) { const type = adaptWireType(schema, pkg); if (go.isPathScalarParameterType(type)) { return type; } throw new Error(`unexpected path scalar parameter type ${schema.type}`); } function adaptQueryScalarParameterType(schema, pkg) { const type = adaptWireType(schema, pkg); if (go.isQueryScalarParameterType(type)) { return type; } throw new Error(`unexpected query scalar parameter type ${schema.type}`); } function adaptURIPrameterType(schema, pkg) { const type = adaptWireType(schema, pkg); if (go.isURIParameterType(type)) { return type; } throw new Error(`unexpected URI parameter type ${schema.type}`); } /** * returns true if the parameter should be passed by value * * @param style the style of the parameter * @param location the location of the parameter * @param param the parameter type (needed for schema) * @returns true if the param should be passed by value */ function calculateParamByValue(param, style, location) { return helpers.isTypePassedByValue(param.schema) ? true : go.isRequiredParameter(style) || (location === 'client' && go.isClientSideDefault(style)); } function adaptClient(group, pkg) { const description = `${group.language.go.clientName} contains the methods for the ${group.language.go.name} group.`; const client = new go.Client(pkg, group.language.go.clientName, { description: description }); if (group.language.go.complexHostParams) { client.instance = new go.TemplatedHost(group.language.go.host); } if (group.language.go.hostParams) { for (const hostParam of values(group.language.go.hostParams)) { const style = adaptParameterStyle(hostParam, pkg); const location = adaptMethodLocation(hostParam.implementation); const uriParam = new go.URIParameter(hostParam.language.go.name, hostParam.language.go.serializedName, adaptURIPrameterType(hostParam.schema, pkg), style, calculateParamByValue(hostParam, style, location), location); client.parameters.push(uriParam); } } return client; } function adaptMethodLocation(location) { if (!location) { return 'method'; } switch (location) { case m4.ImplementationLocation.Client: return 'client'; case m4.ImplementationLocation.Method: return 'method'; default: throw new Error(`unhandled parameter location type ${location}`); } } function adaptMethodParameters(op, method) { if (!op.parameters) { return; } for (const param of values(helpers.aggregateParameters(op))) { const methodParam = adaptMethodParameter(op, param, method.receiver.type); method.parameters.push(methodParam); } } function adaptResponseEnvelope(m4CodeModel, op, pkg, forMethod) { const respEnvSchema = op.language.go.responseEnv; const respEnv = new go.ResponseEnvelope(respEnvSchema.language.go.name, { description: respEnvSchema.language.go.description }, forMethod); // add any headers for (const prop of values(respEnvSchema.properties)) { if (prop.language.go.fromHeader) { let headerResp; if (prop.schema.language.go.headerCollectionPrefix) { const headerType = adaptWireType(prop.schema, forMethod.receiver.type.pkg, false); if (headerType.kind !== 'map') { throw new Error(`unexpected type ${headerType.kind} for HeaderMapResponse ${prop.language.go.name}`); } headerResp = new go.HeaderMapResponse(prop.language.go.name, headerType, prop.schema.language.go.headerCollectionPrefix); } else { headerResp = new go.HeaderScalarResponse(prop.language.go.name, adaptHeaderScalarType(prop.schema, false, pkg), prop.language.go.fromHeader, prop.language.go.byValue); } if (hasDescription(prop.language.go)) { headerResp.docs.description = prop.language.go.description; } respEnv.headers.push(headerResp); } } if (!respEnvSchema.language.go.resultProp) { return respEnv; } // now add the result field const resultProp = respEnvSchema.language.go.resultProp; if (helpers.isMultiRespOperation(op)) { respEnv.result = adaptAnyResult(op, pkg); } else if (resultProp.schema.type === m4.SchemaType.Binary) { respEnv.result = new go.BinaryResult(resultProp.language.go.name); } else if (m4CodeModel.language.go.headAsBoolean && op.requests[0].protocol.http.method === 'head') { respEnv.result = new go.HeadAsBooleanResult(resultProp.language.go.name); } else if (!resultProp.language.go.embeddedType) { const resultType = adaptWireType(resultProp.schema, forMethod.receiver.type.pkg); if (go.isMonomorphicResultType(resultType)) { respEnv.result = new go.MonomorphicResult(resultProp.language.go.name, adaptResultFormat(helpers.getSchemaResponse(op).protocol), resultType, resultProp.language.go.byValue); respEnv.result.xml = adaptXMLInfo(resultProp.schema); } else { throw new Error(`invalid monomorphic result type ${resultType.kind}`); } } else if (resultProp.isDiscriminator) { let ifaceResult; for (const iface of values(forMethod.receiver.type.pkg.interfaces)) { if (iface.name === resultProp.schema.language.go.name) { ifaceResult = iface; break; } } if (!ifaceResult) { throw new Error(`didn't find InterfaceType for result property ${resultProp.schema.language.go.name}`); } respEnv.result = new go.PolymorphicResult(ifaceResult); } else if (helpers.getSchemaResponse(op)) { /** * The modelType will be a PolymorphicModel when the response envelope * is a concrete type from a polymorphic hierarchy */ let modelType; for (const model of forMethod.receiver.type.pkg.models) { if ((model.kind === 'model' || model.kind === 'polymorphicModel') && model.name === resultProp.schema.language.go.name) { modelType = model; break; } } if (!modelType) { throw new Error(`didn't find type name ${resultProp.schema.language.go.name} for response envelope ${respEnv.name}`); } const resultFormat = adaptResultFormat(helpers.getSchemaResponse(op).protocol); if (resultFormat !== 'JSON' && resultFormat !== 'XML') { throw new Error(`unexpected result format ${resultFormat} for model ${modelType.name}`); } respEnv.result = new go.ModelResult(modelType, resultFormat); } else { throw new Error(`unhandled result type for operation ${op.language.go.name}`); } if (hasDescription(resultProp.language.go)) { respEnv.result.docs.description = resultProp.language.go.description; } return respEnv; } function adaptMethodNaming(op) { const info = op.language.go; return new go.MethodNaming(info.protocolNaming.internalMethod, info.protocolNaming.requestMethod, info.protocolNaming.responseMethod); } function getStatusCodes(op) { // concat all status codes that return the same schema into one array. // this is to support operations that specify multiple response codes // that return the same schema (or no schema). let statusCodes = new Array(); for (const resp of values(op.responses)) { statusCodes = statusCodes.concat(parseInt(resp.protocol.http?.statusCodes)); } if (statusCodes.length === 0) { // if the operation defines no status codes (which is non-conformant) // then add 200, 201, 202, and 204 to the list. this is to accomodate // some quirky tests in the test server. // TODO: https://github.com/Azure/autorest.go/issues/659 statusCodes = [200, 201, 202, 204]; } return statusCodes; } function adaptMethodParameter(op, param, client) { let adaptedParam; let location = 'method'; const clientParamKey = `${client.name}-${param.language.go.name}`; if (param.implementation === m4.ImplementationLocation.Client) { // check if we've already adapted this client parameter // TODO: grouped client params const clientParam = clientParams.get(clientParamKey); if (clientParam) { return clientParam; } location = 'client'; } const pkg = client.pkg; const style = adaptParameterStyle(param, pkg); // unfortunately param.language.go!.byValue isn't always populated. // since we can't trust it, we calculate the value instead. const byValue = calculateParamByValue(param, style, location); switch (param.protocol.http?.in) { case 'body': { if (!op.requests[0].protocol.http.mediaTypes) { throw new Error(`no media types defined for operation ${op.operationId}`); } const contentTypeLiteral = op.requests[0].protocol.http.mediaTypes[0]; let contentType = new go.Literal(new go.String(), contentTypeLiteral); if (op.requests[0].protocol.http.mediaTypes.length > 1) { for (const param of values(op.requests[0].parameters)) { // If a request defined more than one possible media type, then the param is expected to be synthesized from modelerfour // and should be a SealedChoice schema type that account for the acceptable media types defined in the swagger. if (param.origin === 'modelerfour:synthesized/content-type' && param.schema.type === m4.SchemaType.SealedChoice) { contentType = new go.ParameterRef(param.language.go.name); } } } const bodyType = adaptWireType(param.schema, pkg); if (op.requests[0].protocol.http.knownMediaType === KnownMediaType.Form) { const collectionFormat = adaptCollectionFormat(param); if (collectionFormat) { if (bodyType.kind !== 'slice') { throw new Error(`unexpected type ${bodyType.kind} for FormBodyCollectionParameter ${param.language.go.name}`); } adaptedParam = new go.FormBodyCollectionParameter(param.language.go.name, param.language.go.serializedName, bodyType, collectionFormat, style, byValue); } else { adaptedParam = new go.FormBodyScalarParameter(param.language.go.name, param.language.go.serializedName, bodyType, style, byValue); } } else if (op.requests[0].protocol.http.knownMediaType === KnownMediaType.Multipart) { adaptedParam = new go.MultipartFormBodyParameter(param.language.go.name, bodyType, style, byValue); } else { const format = adaptBodyFormat(op.requests[0].protocol); adaptedParam = new go.BodyParameter(param.language.go.name, format, contentType, bodyType, style, byValue); adaptedParam.xml = adaptXMLInfo(param.schema); } break; } case 'header': { const collectionFormat = adaptCollectionFormat(param); if (param.schema.language.go.headerCollectionPrefix) { const headerType = adaptWireType(param.schema, pkg, true); if (headerType.kind !== 'map') { throw new Error(`unexpected type ${headerType.kind} for HeaderMapParameter ${param.language.go.name}`); } adaptedParam = new go.HeaderMapParameter(param.language.go.name, param.schema.language.go.headerCollectionPrefix, headerType, style, byValue, location); } else if (collectionFormat) { const headerType = adaptWireType(param.schema, pkg, true); if (headerType.kind !== 'slice') { throw new Error(`unexpected type ${headerType.kind} for HeaderCollectionParameter ${param.language.go.name}`); } adaptedParam = new go.HeaderCollectionParameter(param.language.go.name, param.language.go.serializedName, headerType, collectionFormat, style, byValue, location); } else { adaptedParam = new go.HeaderScalarParameter(param.language.go.name, param.language.go.serializedName, adaptHeaderScalarType(param.schema, true, pkg), style, byValue, location); } break; } case 'path': { const collectionFormat = adaptCollectionFormat(param); if (collectionFormat) { const pathType = adaptWireType(param.schema, pkg); if (pathType.kind !== 'slice') { throw new Error(`unexpected type ${pathType.kind} for PathCollectionParameter ${param.language.go.name}`); } adaptedParam = new go.PathCollectionParameter(param.language.go.name, param.language.go.serializedName, !skipURLEncoding(param), pathType, collectionFormat, style, byValue, location); } else { const skipUrlEncoding = skipURLEncoding(param); adaptedParam = new go.PathScalarParameter(param.language.go.name, param.language.go.serializedName, !skipUrlEncoding, adaptPathScalarParameterType(param.schema, pkg), style, byValue, location); // this is a legacy hack to work around the fact that // swagger doesn't allow path params to be empty. adaptedParam.omitEmptyStringCheck = skipUrlEncoding && (adaptedParam.type.kind === 'string' || (adaptedParam.type.kind === 'constant' && adaptedParam.type.type === 'string')); } break; } case 'query': { const collectionFormat = adaptExtendedCollectionFormat(param); if (collectionFormat) { const queryType = adaptWireType(param.schema, pkg); if (queryType.kind !== 'slice') { throw new Error(`unexpected type ${queryType.kind} for QueryCollectionParameter ${param.language.go.name}`); } adaptedParam = new go.QueryCollectionParameter(param.language.go.name, param.language.go.serializedName, !skipURLEncoding(param), queryType, collectionFormat, style, byValue, location); } else { adaptedParam = new go.QueryScalarParameter(param.language.go.name, param.language.go.serializedName, !skipURLEncoding(param), adaptQueryScalarParameterType(param.schema, pkg), style, byValue, location); } break; } case 'uri': adaptedParam = new go.URIParameter(param.language.go.name, param.language.go.serializedName, adaptURIPrameterType(param.schema, pkg), style, byValue, adaptParameterlocation(param)); break; default: { if (param.protocol.http?.in) { throw new Error(`unhandled parameter location type ${param.protocol.http.in}`); } // this is a synthesized parameter (e.g. ResumeToken) if (param.language.go.isResumeToken) { adaptedParam = new go.ResumeTokenParameter(); } else { throw new Error(`unknown parameter in operation ${op.language.go.name}`); } } } if (hasDescription(param.language.go)) { adaptedParam.docs.description = param.language.go.description; } if (param.origin === 'modelerfour:synthesized/api-version') { if (adaptedParam.type.kind === 'literal' && adaptedParam.type.type.kind === 'string') { const versionLiteral = adaptedParam.type; // check if we already have a ConstantDef for this API version. let versionConst = client.apiVersions.find((e) => e.literal.literal === versionLiteral.literal); if (!versionConst) { versionConst = new go.ConstantDef(`version${ensureNameCase(versionLiteral.literal)}`, versionLiteral); client.apiVersions.push(versionConst); } adaptedParam.type = new go.Literal(versionConst, versionConst.name); } else { throw new Error(`unexpected parameter type kind ${adaptedParam.type.kind} for API version`); } } // track client parameter for later use if (adaptedParam.location === 'client') { clientParams.set(clientParamKey, adaptedParam); } if (param.language.go.paramGroup) { const paramGroup = findOrAdaptParamsGroup(param, pkg); // parameter groups can be shared across methods so don't add any duplicate parameters if (values(paramGroup.params) .where((each) => { return each.name === adaptedParam.name; }) .count() === 0) { paramGroup.params.push(adaptedParam); } if (adaptedParam.style === 'required') { // if at least one param within a group is required then the group must be required. // however, it's possible that the param group was initially created from a non-required // param. so we need to be sure to update it as required. paramGroup.required = true; } adaptedParam.group = paramGroup; } return adaptedParam; } function adaptCollectionFormat(param) { switch (param.protocol.http?.style) { case m4.SerializationStyle.PipeDelimited: return 'pipes'; case m4.SerializationStyle.Simple: return 'csv'; case m4.SerializationStyle.SpaceDelimited: return 'ssv'; case m4.SerializationStyle.TabDelimited: return 'tsv'; default: return undefined; } } function adaptExtendedCollectionFormat(param) { switch (param.protocol.http?.style) { case m4.SerializationStyle.Form: if (param.protocol.http?.explode === true) { return 'multi'; } return 'csv'; case m4.SerializationStyle.PipeDelimited: return 'pipes'; case m4.SerializationStyle.Simple: return 'csv'; case m4.SerializationStyle.SpaceDelimited: return 'ssv'; case m4.SerializationStyle.TabDelimited: return 'tsv'; default: return undefined; } } // returns true if the parameter should not be URL encoded function skipURLEncoding(param) { if (param.extensions) { return param.extensions['x-ms-skip-url-encoding'] === true; } return false; } function adaptParameterlocation(param) { if (!param.implementation) { return 'method'; } switch (param.implementation) { case m4.ImplementationLocation.Client: return 'client'; case m4.ImplementationLocation.Method: return 'method'; default: throw new Error(`unhandled parameter location ${param.implementation}`); } } function adaptParameterStyle(param, pkg) { if (param.clientDefaultValue) { const adaptedType = adaptWireType(param.schema, pkg); if (!go.isLiteralValueType(adaptedType)) { throw new Error(`unsupported client side default type ${adaptedType.kind} for parameter ${param.language.go.name}`); } return new go.ClientSideDefault(new go.Literal(adaptedType, param.clientDefaultValue)); } else if (param.schema.type === m4.SchemaType.Constant) { if (param.required) { return 'literal'; } return 'flag'; } else if (param.required === true) { return 'required'; } else { return 'optional'; } } function findOrAdaptParamsGroup(param, pkg) { const groupProp = param.language.go.paramGroup; let paramGroup = paramGroups.get(groupProp.schema.language.go.name); if (!paramGroup) { paramGroup = adaptParameterGroup(groupProp, pkg, adaptParameterlocation(param)); paramGroups.set(groupProp.schema.language.go.name, paramGroup); } return paramGroup; } function adaptParameterGroup(groupProp, pkg, location) { const paramGroup = new go.ParameterGroup(pkg, groupProp.language.go.name, groupProp.schema.language.go.name, groupProp.required === true, location); paramGroup.docs.description = groupProp.language.go.description; return paramGroup; } // returns the media type used by the protocol function adaptBodyFormat(protocol) { switch (protocol.http.knownMediaType) { case KnownMediaType.Json: return 'JSON'; case KnownMediaType.Xml: return 'XML'; case KnownMediaType.Binary: return 'binary'; case KnownMediaType.Text: return 'Text'; default: throw new Error(`unhandled body protocol format ${protocol.http.knownMediaType}`); } } function adaptAnyResult(op, pkg) { const resultTypes = {}; for (const resp of values(op.responses)) { let wireType; if (helpers.isSchemaResponse(resp)) { wireType = adaptWireType(resp.schema, pkg); } else { // the operation contains a mix of schemas and non-schema responses continue; } resultTypes[parseInt(resp.protocol.http.statusCodes)] = wireType; } return new go.AnyResult('Value', 'JSON', resultTypes); } function adaptResultFormat(protocol) { switch (protocol.http.knownMediaType) { case KnownMediaType.Json: return 'JSON'; case KnownMediaType.Xml: return 'XML'; case KnownMediaType.Text: return 'Text'; default: throw new Error(`unhandled result protocol format ${protocol.http.knownMediaType}`); } } //# sourceMappingURL=clients.js.map