UNPKG

@autorest/go

Version:
988 lines 98.2 kB
/*--------------------------------------------------------------------------------------------- * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ import * as go from '../../../codemodel.go/src/index.js'; import * as naming from '../../../naming.go/src/naming.js'; import * as helpers from './helpers.js'; import { ImportManager } from './imports.js'; import { CodegenError } from './errors.js'; // represents the generated content for an operation group export class OperationGroupContent { name; content; constructor(name, content) { this.name = name; this.content = content; } } /** * Creates the content for all the *_client.go files. * * @param pkg contains the package content * @param target the codegen target for the module * @param options the emitter options * @returns the text for the files or the empty string */ export function generateOperations(pkg, target, options) { // generate protocol operations const operations = new Array(); if (pkg.clients.length === 0) { return operations; } const azureARM = target === 'azure-arm'; for (const client of pkg.clients) { // the list of packages to import const imports = new ImportManager(pkg); if (client.methods.length > 0) { // add standard imports for clients with methods. // clients that are purely hierarchical (i.e. having no APIs) won't need them. imports.add('net/http'); imports.add('github.com/Azure/azure-sdk-for-go/sdk/azcore/policy'); imports.add('github.com/Azure/azure-sdk-for-go/sdk/azcore/runtime'); } imports.add(azureARM ? 'github.com/Azure/azure-sdk-for-go/sdk/azcore/arm' : 'github.com/Azure/azure-sdk-for-go/sdk/azcore'); // generate client type let clientText = helpers.formatDocComment(client.docs); clientText += "// Don't use this type directly, use "; if (client.instance?.kind === 'constructable' && client.instance.constructors.length === 1) { clientText += `${client.instance.constructors[0].name}() instead.\n`; } else if (client.parent) { // find the accessor method let accessorMethod; for (const clientAccessor of client.parent.clientAccessors) { if (clientAccessor.returns === client) { accessorMethod = clientAccessor.name; break; } } if (!accessorMethod) { throw new CodegenError('InternalError', `didn't find accessor method for client ${client.name} on parent client ${client.parent.name}`); } clientText += `[${client.parent.name}.${accessorMethod}] instead.\n`; } else { clientText += 'a constructor function instead.\n'; } if (client.apiVersions.length > 0) { clientText += `//\n// Generated from API version`; if (client.apiVersions.length > 1) { clientText += `s ${client.apiVersions .map((v) => v.literal.literal) .sort() .join(', ')}\n`; } else { clientText += ` ${client.apiVersions[0].literal.literal}\n`; } } const indent = new helpers.Indentation(); clientText += `type ${client.name} struct {\n`; clientText += `${indent.get()}internal *${azureARM ? 'arm' : 'azcore'}.Client\n`; // check for any optional host params const optionalParams = new Array(); const isParamPointer = function (param) { // for client params, only optional and flag types are passed by pointer return param.style === 'flag' || param.style === 'optional'; }; // now emit any client params (non parameterized host params case) if (client.parameters.length > 0) { const addedGroups = new Set(); for (const clientParam of client.parameters) { if (go.isLiteralParameter(clientParam.style)) { continue; } if (clientParam.group) { if (!addedGroups.has(clientParam.group.groupName)) { clientText += `${indent.get()}${naming.uncapitalize(clientParam.group.groupName)} ${!isParamPointer(clientParam) ? '' : '*'}${clientParam.group.groupName}\n`; addedGroups.add(clientParam.group.groupName); } continue; } clientText += `${indent.get()}${clientParam.name} `; if (!isParamPointer(clientParam)) { clientText += `${go.getTypeDeclaration(clientParam.type, client.pkg)}\n`; } else { clientText += `${helpers.formatParameterTypeName(client.pkg, clientParam)}\n`; } if (!go.isRequiredParameter(clientParam.style)) { optionalParams.push(clientParam); } } } // end of client definition clientText += '}\n\n'; clientText += generateConstructors(client, target, imports, indent); // generate client accessors and operations let opText = ''; for (const clientAccessor of client.clientAccessors) { imports.addForType(clientAccessor.returns); const subClientDecl = go.getTypeDeclaration(clientAccessor.returns, pkg); opText += helpers.formatDocComment(clientAccessor.docs); opText += `func (client *${client.name}) ${clientAccessor.name}(${getAPIParametersSig(clientAccessor, imports)}) *${subClientDecl} {\n`; opText += `${indent.get()}return &${subClientDecl}{\n`; const initFields = new Array('internal: client.internal'); // propagate all client params for (const param of clientAccessor.parameters) { // by convention, the client accessor params have the // same name as their corresponding client fields. initFields.push(`${param.name}: ${param.name}`); } // accessor params and client fields are mutually exclusive // so we don't need to worry about potentials for duplication. for (const param of client.parameters) { if (go.isLiteralParameter(param.style)) { continue; } else if (clientAccessor.returns.parameters.some((p) => p.name === param.name)) { // only propagate ctor params that are common between parent/child initFields.push(`${param.name}: client.${param.name}`); } } initFields.sort(); indent.push(); for (const initField of initFields) { opText += `${indent.get()}${initField},\n`; } indent.pop(); opText += `${indent.get()}}\n}\n\n`; } const nextPageMethods = new Array(); for (const method of client.methods) { // protocol creation can add imports to the list so // it must be done before the imports are written out if (go.isLROMethod(method)) { // generate Begin method opText += generateLROBeginMethod(method, options, imports, indent); } opText += generateOperation(method, options, imports, indent); opText += createProtocolRequest(azureARM, method, imports, indent); if (method.kind !== 'lroMethod') { // LRO responses are handled elsewhere, with the exception of pageable LROs opText += createProtocolResponse(method, imports, indent); } if (go.isPageableMethod(method) && method.strategy?.kind === 'nextLink' && method.strategy.method && !nextPageMethods.includes(method.strategy.method)) { // track the next page methods to generate as multiple operations can use the same next page operation nextPageMethods.push(method.strategy.method); } } for (const method of nextPageMethods) { opText += createProtocolRequest(azureARM, method, imports, indent); } // stitch it all together let text = helpers.contentPreamble(pkg); text += imports.text(); text += clientText; text += opText; operations.push(new OperationGroupContent(client.name, text)); } return operations; } /** * generates all modeled client constructors and client options types. * if there are no client constructors, the empty string is returned. * * @param client the client for which to generate constructors and the client options type * @param imports the import manager currently in scope * @returns the client constructor code or the empty string */ function generateConstructors(client, type, imports, indent) { if (client.instance?.kind !== 'constructable') { return ''; } const clientOptions = client.instance.options; let ctorText = ''; if (clientOptions.kind === 'clientOptions') { // for non-ARM, the options type will always be a parameter group ctorText += `// ${clientOptions.name} contains the optional values for creating a [${client.name}].\n`; ctorText += `type ${clientOptions.name} struct {\n${indent.get()}azcore.ClientOptions\n`; for (const param of clientOptions.parameters) { if (go.isAPIVersionParameter(param)) { // we use azcore.ClientOptions.APIVersion continue; } ctorText += helpers.formatDocCommentWithPrefix(naming.ensureNameCase(param.name), param.docs); if (go.isClientSideDefault(param.style)) { if (!param.docs.description && !param.docs.summary) { ctorText += '\n'; } ctorText += `${indent.get()}${helpers.comment(`The default value is ${helpers.formatLiteralValue(param.style.defaultValue, false)}`, '// ')}.\n`; } ctorText += `${indent.get()}${naming.ensureNameCase(param.name)} *${go.getTypeDeclaration(param.type, client.pkg)}\n`; } ctorText += '}\n\n'; } for (const constructor of client.instance.constructors) { const ctorParams = new Array(); const paramDocs = new Array(); // ctor params can also be present in the supplemental endpoint parameters const consolidatedCtorParams = new Array(); if (client.instance.endpoint) { consolidatedCtorParams.push(client.instance.endpoint.parameter); if (client.instance.endpoint.supplemental) { consolidatedCtorParams.push(...client.instance.endpoint.supplemental.parameters); } } for (const param of helpers.sortClientParameters(constructor.parameters, type)) { if (!consolidatedCtorParams.includes(param)) { consolidatedCtorParams.push(param); } } for (const ctorParam of consolidatedCtorParams) { if (!go.isRequiredParameter(ctorParam.style)) { // param is part of the options group continue; } imports.addForType(ctorParam.type); ctorParams.push(`${ctorParam.name} ${helpers.formatParameterTypeName(client.pkg, ctorParam)}`); if (ctorParam.docs.summary || ctorParam.docs.description) { paramDocs.push(helpers.formatCommentAsBulletItem(ctorParam.name, ctorParam.docs)); } } const emitProlog = function (optionsTypeName, tokenAuth, plOpts) { imports.add('github.com/Azure/azure-sdk-for-go/sdk/azcore/runtime'); let bodyText = `${indent.get()}if options == nil {\n`; bodyText += `${indent.push().get()}options = &${optionsTypeName}{}\n`; bodyText += `${indent.pop().get()}}\n`; let apiVersionConfig = ''; // check if there's an api version parameter let apiVersionParam; for (const param of consolidatedCtorParams) { switch (param.kind) { case 'headerScalarParam': case 'pathScalarParam': case 'queryScalarParam': case 'uriParam': if (param.isApiVersion) { apiVersionParam = param; } } } if (tokenAuth) { imports.add('github.com/Azure/azure-sdk-for-go/sdk/azcore/cloud'); imports.add('fmt'); imports.add('reflect'); bodyText += `${indent.get()}if reflect.ValueOf(options.Cloud).IsZero() {\n`; bodyText += `${indent.push().get()}options.Cloud = cloud.AzurePublic\n`; bodyText += `${indent.pop().get()}}\n`; bodyText += `${indent.get()}c, ok := options.Cloud.Services[ServiceName]\n`; bodyText += `${indent.get()}if !ok {\n`; bodyText += `${indent.push().get()}return nil, fmt.Errorf("provided Cloud field is missing configuration for %s", ServiceName)\n`; bodyText += `${indent.pop().get()}} else if c.Audience == "" {\n`; bodyText += `${indent.push().get()}return nil, fmt.Errorf("provided Cloud field is missing Audience for %s", ServiceName)\n`; bodyText += `${indent.pop().get()}}\n`; } if (apiVersionParam) { let location; let name; switch (apiVersionParam.kind) { case 'headerScalarParam': location = 'Header'; name = apiVersionParam.headerName; break; case 'pathScalarParam': case 'uriParam': location = 'Path'; // name isn't used for the path case break; case 'queryScalarParam': location = 'QueryParam'; name = apiVersionParam.queryParameter; break; } indent.push(); // level 2 for PipelineOptions fields if (name) { indent.push(); // level 3 for APIVersionOptions fields name = `\n${indent.get()}Name: "${name}",`; indent.pop(); // back to level 2 } else { name = ''; } indent.push(); // level 3 for APIVersionOptions fields apiVersionConfig = `\n${indent.pop().get()}APIVersion: runtime.APIVersionOptions{${name}\n${indent.push().get()}Location: runtime.APIVersionLocation${location},\n${indent.pop().get()}},`; indent.pop(); // back to level 1 if (!plOpts) { apiVersionConfig += '\n'; } } bodyText += `${indent.get()}cl, err := azcore.NewClient(moduleName, moduleVersion, runtime.PipelineOptions{${apiVersionConfig}${plOpts ?? ''}}, &options.ClientOptions)\n`; return bodyText; }; // check if there's a credential parameter let credentialParam; for (const param of constructor.parameters) { if (param.kind === 'credentialParam') { credentialParam = param; break; } } let prolog; if (credentialParam) { switch (credentialParam.type.kind) { case 'tokenCredential': imports.add('github.com/Azure/azure-sdk-for-go/sdk/azcore'); paramDocs.push(helpers.formatCommentAsBulletItem('credential', { summary: 'used to authorize requests. Usually a credential from azidentity.' })); switch (clientOptions.kind) { case 'clientOptions': { imports.add('github.com/Azure/azure-sdk-for-go/sdk/azcore/policy'); indent.push(); // level 2 for PipelineOptions fields indent.push(); // level 3 for BearerTokenOptions fields const tokenPolicyOpts = `&policy.BearerTokenOptions{\n${indent.get()}InsecureAllowCredentialWithHTTP: options.InsecureAllowCredentialWithHTTP,\n${indent.pop().get()}}`; // we assume a single scope. this is enforced when adapting the data from tcgc const tokenPolicy = `\n${indent.get()}PerCall: []policy.Policy{\n${indent.get()}runtime.NewBearerTokenPolicy(credential, []string{c.Audience + "${helpers.splitScope(credentialParam.type.scopes[0]).scope}"}, ${tokenPolicyOpts}),\n${indent.get()}},\n`; indent.pop(); // back to level 1 prolog = emitProlog(go.getTypeDeclaration(clientOptions, client.pkg), true, tokenPolicy); break; } case 'armClientOptions': // this is the ARM case imports.add('github.com/Azure/azure-sdk-for-go/sdk/azcore/arm'); prolog = `${indent.get()}cl, err := arm.NewClient(moduleName, moduleVersion, credential, options)\n`; break; } break; } } else { prolog = emitProlog(go.getTypeDeclaration(clientOptions, client.pkg), false); } // add client options last ctorParams.push(`options ${helpers.formatParameterTypeName(client.pkg, clientOptions)}`); paramDocs.push(helpers.formatCommentAsBulletItem('options', { summary: 'Contains optional client configuration. Pass nil to accept the default values.' })); ctorText += `// ${constructor.name} creates a new instance of ${client.name} with the specified values.\n`; for (const doc of paramDocs) { ctorText += doc; } ctorText += `func ${constructor.name}(${ctorParams.join(', ')}) (*${client.name}, error) {\n`; ctorText += prolog; ctorText += `${indent.get()}if err != nil {\n`; ctorText += `${indent.push().get()}return nil, err\n`; ctorText += `${indent.pop().get()}}\n`; // handle any client-side defaults if (clientOptions.kind === 'clientOptions') { for (const param of clientOptions.parameters) { if (go.isClientSideDefault(param.style)) { let name; if (go.isAPIVersionParameter(param)) { name = 'APIVersion'; } else { name = naming.ensureNameCase(param.name); } ctorText += `${indent.get()}${param.name} := ${helpers.formatLiteralValue(param.style.defaultValue, false)}\n`; ctorText += `${indent.get()}if options.${name} != ${helpers.zeroValue(param)} {\n`; ctorText += `${indent.push().get()}${param.name} = ${helpers.star(param.byValue)}options.${name}\n`; ctorText += `${indent.pop().get()}}\n`; } } } // construct the supplemental path and join it to the endpoint if (client.instance.endpoint?.supplemental) { // the endpoint param is always the first ctor param const endpointParam = client.instance.constructors[0].parameters[0]; if (client.instance.endpoint.supplemental.parameters.length > 0) { imports.add('strings'); ctorText += `${indent.get()}host := "${client.instance.endpoint.supplemental.path}"\n`; for (const param of client.instance.endpoint.supplemental.parameters) { ctorText += `${indent.get()}host = strings.ReplaceAll(host, "{${param.uriPathSegment}}", ${helpers.formatValue(param.name, param.type, imports)})\n`; } ctorText += `${indent.get()}${endpointParam.name} = runtime.JoinPaths(${endpointParam.name}, host)\n`; } else { // there are no params for the supplemental host, so just append it ctorText += `${indent.get()}${endpointParam.name} = runtime.JoinPaths(${endpointParam.name}, "${client.instance.endpoint.supplemental.path}")\n`; } } // construct client literal let clientVar = 'client'; // ensure clientVar doesn't collide with any params for (const param of consolidatedCtorParams) { if (param.name === clientVar) { clientVar = naming.ensureNameCase(client.name, true); break; } } ctorText += `${indent.get()}${clientVar} := &${client.name}{\n`; // NOTE: we don't enumerate consolidatedCtorParams here // as any supplemental endpoint params are ephemeral and // consumed during client construction. indent.push(); for (const parameter of client.parameters) { if (go.isLiteralParameter(parameter.style)) { continue; } // each client field will have a matching parameter with the same name ctorText += `${indent.get()}${parameter.name}: ${parameter.name},\n`; } ctorText += `${indent.get()}internal: cl,\n`; indent.pop(); ctorText += `${indent.get()}}\n`; ctorText += `${indent.get()}return ${clientVar}, nil\n`; ctorText += '}\n\n'; } return ctorText; } // use this to generate the code that will help process values returned in response headers function formatHeaderResponseValue(method, headerResp, respObj, zeroResp, imports, indent) { // dictionaries are handled slightly different so we do that first if (headerResp.kind === 'headerMapResponse') { imports.add('github.com/Azure/azure-sdk-for-go/sdk/azcore/to'); imports.add('strings'); const headerPrefix = headerResp.headerName; let text = `${indent.get()}for hh := range resp.Header {\n`; text += `${indent.push().get()}if len(hh) > len("${headerPrefix}") && strings.EqualFold(hh[:len("${headerPrefix}")], "${headerPrefix}") {\n`; text += `${indent.push().get()}if ${respObj}.${headerResp.fieldName} == nil {\n`; text += `${indent.push().get()}${respObj}.${headerResp.fieldName} = map[string]*string{}\n`; text += `${indent.pop().get()}}\n`; text += `${indent.get()}${respObj}.${headerResp.fieldName}[hh[len("${headerPrefix}"):]] = to.Ptr(resp.Header.Get(hh))\n`; text += `${indent.pop().get()}}\n`; text += `${indent.pop().get()}}\n`; return text; } let text = `${indent.get()}if val := resp.Header.Get("${headerResp.headerName}"); val != "" {\n`; indent.push(); let name = naming.uncapitalize(headerResp.fieldName); let byRef = '&'; switch (headerResp.type.kind) { case 'constant': case 'etag': text += `${indent.get()}${respObj}.${headerResp.fieldName} = (*${go.getTypeDeclaration(headerResp.type, method.receiver.type.pkg)})(&val)\n`; indent.pop(); text += `${indent.get()}}\n`; return text; case 'encodedBytes': // a base-64 encoded value in string format imports.add('encoding/base64'); text += `${indent.get()}${name}, err := base64.${helpers.formatBytesEncoding(headerResp.type.encoding)}Encoding.DecodeString(val)\n`; byRef = ''; break; case 'literal': text += `${indent.get()}${respObj}.${headerResp.fieldName} = &val\n`; indent.pop(); text += `${indent.get()}}\n`; return text; case 'scalar': text += emitScalarParsing(headerResp.type, 'val', name, imports, indent); break; case 'string': text += `${indent.get()}${respObj}.${headerResp.fieldName} = &val\n`; text += `${indent.pop().get()}}\n`; return text; case 'time': imports.add('time'); switch (headerResp.type.format) { case 'RFC1123': case 'RFC3339': text += `${indent.get()}${name}, err := time.Parse(${headerResp.type.format === 'RFC1123' ? helpers.RFC1123Format : helpers.RFC3339Format}, val)\n`; break; case 'PlainDate': text += `${indent.get()}${name}, err := time.Parse(${helpers.plainDateFormat}, val)\n`; break; case 'PlainTime': text += `${indent.get()}${name}, err := time.Parse(${helpers.plainTimeFormat}, val)\n`; break; case 'Unix': imports.add('strconv'); imports.add('github.com/Azure/azure-sdk-for-go/sdk/azcore/to'); text += `${indent.get()}sec, err := strconv.ParseInt(val, 10, 64)\n`; name = 'to.Ptr(time.Unix(sec, 0))'; byRef = ''; break; } } // NOTE: only cases that required parsing will fall through to here text += `${indent.get()}if err != nil {\n`; text += `${indent.push().get()}return ${zeroResp}, err\n`; text += `${indent.pop().get()}}\n`; text += `${indent.get()}${respObj}.${headerResp.fieldName} = ${byRef}${name}\n`; text += `${indent.pop().get()}}\n`; return text; } /** * emits the code for parsing scalar types from a string. * note that the parsing error result is placed into a * local var named "err". * * @param scalar the type of scalar to parse * @param src the source var that contains the scalar in string format * @param dst the destination var that contains the result * @param imports the import manager currently in scope * @param indent the indentation helper currently in scope * @returns the scalar parsing code */ function emitScalarParsing(scalar, src, dst, imports, indent) { imports.add('strconv'); switch (scalar.type) { case 'bool': return `${indent.get()}${dst}, err := strconv.ParseBool(${src})\n`; case 'float32': return `${indent.get()}${dst}32, err := strconv.ParseFloat(${src}, 32)\n` + `${indent.get()}${dst} := float32(${dst}32)\n`; case 'float64': return `${indent.get()}${dst}, err := strconv.ParseFloat(${src}, 64)\n`; case 'int32': return `${indent.get()}${dst}32, err := strconv.ParseInt(${src}, 10, 32)\n` + `${indent.get()}${dst} := int32(${dst}32)\n`; case 'int64': return `${indent.get()}${dst}, err := strconv.ParseInt(${src}, 10, 64)\n`; default: throw new CodegenError('InternalError', `unhandled scalar type ${scalar.type}`); } } /** * returns the zero value for the specified method. * note that the zero value will be different depending * on which API for the method is being called. * * @param method the method to determine the zero value * @param apiType the method's API that's being invoked * lro - the exported LRO API * op - the operation method. for sync methods this is the exported API. for LROs it's the internal method called by Begin* * handler - the internal response handler method * @returns the zero value */ function getZeroReturnValue(method, apiType) { let returnType = `${method.returns.name}{}`; if (go.isLROMethod(method)) { if (apiType === 'lro' || apiType === 'op') { // the api returns a *Poller[T] // the operation returns an *http.Response returnType = 'nil'; } } return returnType; } /** * Helper function to generate nil checks for a segmented path * e.g. page.Foo != nil && page.Foo.Bar != nil * * @param segments the field segments * @param varName optional variable name containing the fields * @param omitLastSegment when true, omits the last item in segments. * note that this can cause the function to return the empty string when segments.length === 1 * @returns the sequence of nil checks */ function generateNilChecks(segments, varName = 'page', omitLastSegment = false) { const checks = []; let segmentCount = segments.length; if (omitLastSegment) { segmentCount -= 1; } for (let i = 0; i < segmentCount; i++) { const currentPath = [varName, ...segments.map((segment) => segment.name).slice(0, i + 1)].join('.'); checks.push(`${currentPath} != nil`); } return checks.join(' && '); } /** * emits code that calls runtime.NewPager * * @param method the pageable method * @param options emitter options * @param imports the import manager currently in scope * @param indent the indentation helper currently in scope * @returns the complete call to runtime.NewPager(...) */ function emitPagerDefinition(method, options, imports, indent) { imports.add('context'); let text = `runtime.NewPager(runtime.PagingHandler[${method.returns.name}]{\n`; text += `${indent.push().get()}More: func(page ${method.returns.name}) bool {\n`; indent.push(); if (method.strategy) { const moreForNextLinkPath = function (strategy) { const nilChecks = generateNilChecks(strategy.nextLinkPath); const nextLinkPath = helpers.buildNextLinkPath(strategy); text += `${indent.get()}return ${nilChecks} && len(*page.${nextLinkPath}) > 0\n`; }; switch (method.strategy.kind) { case 'continuationToken': { switch (method.strategy.responseToken.kind) { case 'headerScalarResponse': const tokenRespField = method.strategy.responseToken.fieldName; text += `${indent.get()}return page.${tokenRespField} != nil && len(*page.${tokenRespField}) > 0\n`; break; case 'nextLink': moreForNextLinkPath(method.strategy.responseToken); break; default: method.strategy.responseToken; } break; } case 'nextLink': { moreForNextLinkPath(method.strategy); break; } default: method.strategy; } } else { // there is no advancer for single-page pagers text += `${indent.get()}return false\n`; } text += `${indent.pop().get()}},\n`; // end More func text += `${indent.get()}Fetcher: func(ctx context.Context, page *${method.returns.name}) (${method.returns.name}, error) {\n`; indent.push(); const reqParams = helpers.getCreateRequestParameters(method); if (options.generateFakes) { text += `${indent.get()}ctx = context.WithValue(ctx, runtime.CtxAPINameKey{}, "${method.receiver.type.name}.${fixUpMethodName(method)}")\n`; } /** emits code to check the HTTP status code and conditionally call the response handler */ const emitStatusCodeCheckAndResponse = function (respVarName, indent) { let content = `${indent.get()}${helpers.buildIfBlock(indent, { condition: `!runtime.HasStatusCode(${respVarName}, http.StatusOK)`, body: (indent) => `${indent.get()}return ${getZeroReturnValue(method, 'op')}, runtime.NewResponseError(${respVarName})\n`, })}\n`; content += `${indent.get()}return client.${method.naming.responseMethod}(${respVarName})\n`; return content; }; if (method.strategy) { switch (method.strategy.kind) { case 'continuationToken': { const ms = method.strategy; const optionsCopy = 'nextOpts'; text += `${indent.get()}${optionsCopy} := ${method.optionalParamsGroup.groupName}{}\n`; text += `${indent.get()}${helpers.buildIfBlock(indent, { condition: `${method.optionalParamsGroup.name} != nil`, body: (indent) => `${indent.get()}${optionsCopy} = *${method.optionalParamsGroup.name}\n`, })}\n`; let respToken; let nestedNilChecks = ''; switch (ms.responseToken.kind) { case 'headerScalarResponse': respToken = ms.responseToken.fieldName; break; case 'nextLink': respToken = helpers.buildNextLinkPath(ms.responseToken); if (ms.responseToken.nextLinkPath.length > 1) { // we don't need to check the last field for nil as we'll just // assign it to the corresponding field in the options param nestedNilChecks = ` && ${generateNilChecks(ms.responseToken.nextLinkPath, 'page', true)}`; } break; } text += `${indent.get()}${helpers.buildIfBlock(indent, { condition: `page != nil${nestedNilChecks}`, body: (indent) => `${indent.get()}${optionsCopy}.${ms.requestToken.name} = page.${respToken}\n`, })}\n`; text += callCreateRequestWithErrCheck(method, 'req', indent, `&${optionsCopy}`); text += callPipelineDoWithErrCheck(method, 'req', 'resp', indent); text += emitStatusCodeCheckAndResponse('resp', indent); break; } case 'nextLink': { const nextLinkPath = helpers.buildNextLinkPath(method.strategy); let nextLinkVar; if (method.kind === 'pageableMethod') { text += `${indent.get()}nextLink := ""\n`; nextLinkVar = 'nextLink'; text += `${indent.get()}if page != nil {\n`; text += `${indent.push().get()}nextLink = *page.${nextLinkPath}\n`; text += `${indent.pop().get()}}\n`; } else { nextLinkVar = `*page.${nextLinkPath}`; } text += `${indent.get()}resp, err := runtime.FetcherForNextLink(ctx, client.internal.Pipeline(), ${nextLinkVar}, func(ctx context.Context) (*policy.Request, error) {\n`; text += `${indent.push().get()}return client.${method.naming.requestMethod}(${reqParams})\n`; text += `${indent.pop().get()}}, `; // nextPageMethod might be absent in some cases, see https://github.com/Azure/autorest/issues/4393 if (method.strategy.method) { const nextOpParams = helpers.getCreateRequestParametersSig(method.strategy.method).split(','); // keep the parameter names from the name/type tuples and find nextLink param for (let i = 0; i < nextOpParams.length; ++i) { const paramName = nextOpParams[i].trim().split(' ')[0]; const paramType = nextOpParams[i].trim().split(' ')[1]; if (paramName.startsWith('next') && paramType === 'string') { nextOpParams[i] = 'encodedNextLink'; } else { nextOpParams[i] = paramName; } } // add a definition for the nextReq func that uses the nextLinkOperation indent.push(); text += `&runtime.FetcherForNextLinkOptions{\n`; text += `${indent.get()}NextReq: func(ctx context.Context, encodedNextLink string) (*policy.Request, error) {\n`; text += `${indent.push().get()}return client.${method.strategy.method.name}(${nextOpParams.join(', ')})\n`; text += `${indent.pop().get()}},\n`; text += `${indent.pop().get()}})\n`; } else if (method.nextLinkVerb !== 'get') { text += `&runtime.FetcherForNextLinkOptions{\n`; text += `${indent.push().get()}HTTPVerb: http.Method${naming.capitalize(method.nextLinkVerb)},\n`; text += `${indent.pop().get()}})\n`; } else { text += 'nil)\n'; } text += `${indent.get()}if err != nil {\n`; text += `${indent.push().get()}return ${method.returns.name}{}, err\n`; text += `${indent.pop().get()}}\n`; text += `${indent.get()}return client.${method.naming.responseMethod}(resp)\n`; } } } else { // this is the singular page case, no fetcher helper required text += callCreateRequestWithErrCheck(method, 'req', indent); text += callPipelineDoWithErrCheck(method, 'req', 'resp', indent); text += emitStatusCodeCheckAndResponse('resp', indent); } text += `${indent.pop().get()}},\n`; // end Fetcher func if (options.injectSpans) { text += `${indent.get()}Tracer: client.internal.Tracer(),\n`; } text += `${indent.pop().get()}})\n`; // end runtime.NewPager return text; } /** * emits the call to the *CreateRequest method for an SDK method. * if the call returns an error, the error is propagated to the caller. * * @param method the method to call its *CreateRequest method * @param reqVarName the var name of the resultant *policy.Request * @param indent the indentation helper currently in scope * @param optionsParam optional custom param name for the method options param * @returns the call to *CreateRequest with an error check */ function callCreateRequestWithErrCheck(method, reqVarName, indent, optionsParam) { const reqParams = helpers.getCreateRequestParameters(method, optionsParam); let text = `${indent.get()}${reqVarName}, err := client.${method.naming.requestMethod}(${reqParams})\n`; const zeroResp = getZeroReturnValue(method, 'op'); text += `${indent.get()}${helpers.buildErrCheck(indent, 'err', zeroResp)}\n`; return text; } /** * emits the call to the pipeline's Do() method on the internal client. * if the call returns an error, the error is propagated to the caller. * * @param method the method that's making the call * @param reqVarName the var name of the *policy.Request * @param respVarName the var name of the *http.Response * @param indent the indentation helper currently in scope * @returns the call to Do() with an error check */ function callPipelineDoWithErrCheck(method, reqVarName, respVarName, indent) { let text = `${indent.get()}${respVarName}, err := client.internal.Pipeline().Do(${reqVarName})\n`; const zeroResp = getZeroReturnValue(method, 'op'); text += `${indent.get()}${helpers.buildErrCheck(indent, 'err', zeroResp)}\n`; return text; } function genRespErrorDoc(method) { if (!(method.returns.result?.kind === 'headAsBooleanResult') && !go.isPageableMethod(method)) { // when head-as-boolean is enabled, no error is returned for 4xx status codes. // pager constructors don't return an error return '// If the operation fails it returns an *azcore.ResponseError type.\n'; } return ''; } /** * returns the receiver definition for a client * * @param receiver the receiver for which to emit the definition * @returns the receiver definition */ function getClientReceiverDefinition(receiver) { return `(${receiver.name} ${receiver.byValue ? '' : '*'}${receiver.type.name})`; } function generateOperation(method, options, imports, indent) { const params = getAPIParametersSig(method, imports); const returns = generateReturnsInfo(method, 'op'); let methodName = method.name; if (method.kind === 'pageableMethod') { methodName = fixUpMethodName(method); } let text = ''; const respErrDoc = genRespErrorDoc(method); if (method.docs.summary || method.docs.description) { text += helpers.formatDocCommentWithPrefix(methodName, method.docs); } else if (respErrDoc.length > 0) { // if the method has no doc comment but we're adding other // doc comments, add an empty method name comment. this preserves // existing behavior and makes the docs look better overall. text += `// ${methodName} -\n`; } text += respErrDoc; if (go.isLROMethod(method)) { methodName = method.naming.internalMethod; } else { for (const param of helpers.getMethodParameters(method)) { text += helpers.formatCommentAsBulletItem(param.name, param.docs); } } text += `func ${getClientReceiverDefinition(method.receiver)} ${methodName}(${params}) (${returns.join(', ')}) {\n`; if (method.kind === 'pageableMethod') { text += `${indent.get()}return `; text += emitPagerDefinition(method, options, imports, indent); text += '}\n\n'; return text; } text += `${indent.get()}var err error\n`; let operationName = `"${method.receiver.type.name}.${fixUpMethodName(method)}"`; if (options.generateFakes && options.injectSpans) { text += `${indent.get()}const operationName = ${operationName}\n`; operationName = 'operationName'; } if (options.generateFakes) { text += `${indent.get()}ctx = context.WithValue(ctx, runtime.CtxAPINameKey{}, ${operationName})\n`; } if (options.injectSpans) { text += `${indent.get()}ctx, endSpan := runtime.StartSpan(ctx, ${operationName}, client.internal.Tracer(), nil)\n`; text += `${indent.get()}defer func() { endSpan(err) }()\n`; } const zeroResp = getZeroReturnValue(method, 'op'); text += callCreateRequestWithErrCheck(method, 'req', indent); text += callPipelineDoWithErrCheck(method, 'req', 'httpResp', indent); text += `${indent.get()}if !runtime.HasStatusCode(httpResp, ${helpers.formatStatusCodes(method.httpStatusCodes)}) {\n`; indent.push(); text += `${indent.get()}err = runtime.NewResponseError(httpResp)\n`; text += `${indent.get()}return ${zeroResp}, err\n`; text += `${indent.pop().get()}}\n`; // HAB with headers response is handled in protocol responder if (method.returns.result?.kind === 'headAsBooleanResult' && method.returns.headers.length === 0) { text += `${indent.get()}return ${method.returns.name}{${method.returns.result.fieldName}: httpResp.StatusCode >= 200 && httpResp.StatusCode < 300}, nil\n`; } else { if (go.isLROMethod(method)) { text += `${indent.get()}return httpResp, nil\n`; } else if (needsResponseHandler(method)) { // also cheating here as at present the only param to the responder is an http.Response text += `${indent.get()}resp, err := client.${method.naming.responseMethod}(httpResp)\n`; text += `${indent.get()}return resp, err\n`; } else if (method.returns.result?.kind === 'binaryResult') { text += `${indent.get()}return ${method.returns.name}{${method.returns.result.fieldName}: httpResp.Body}, nil\n`; } else { text += `${indent.get()}return ${method.returns.name}{}, nil\n`; } } text += '}\n\n'; return text; } /** * emits Go code to set ContentType on a MultipartContent variable if it has a fixed content type. * handles both direct MultipartContent and slices of MultipartContent. * returns the empty string if there's nothing to set. * * @param paramName the name of the multipart param * @param wireType the underlying type of the multipart param * @param indent the indentation helper currently in scope * @returns setter code or the empty string */ function emitMultipartContentTypeSetter(paramName, wireType, indent) { let text = ''; const unwrapped = helpers.recursiveUnwrapMapSlice(wireType); if (unwrapped.kind !== 'multipartContent' || !unwrapped.contentType) { return text; } switch (wireType.kind) { case 'multipartContent': text += `${indent.get()}${paramName}.ContentType = ${unwrapped.contentType.literal}\n`; break; case 'slice': text += `${indent.get()}for i := range ${paramName} {\n`; text += `${indent.push().get()}${paramName}[i].ContentType = ${unwrapped.contentType.literal}\n`; text += `${indent.pop().get()}}\n`; break; } return text; } function createProtocolRequest(azureARM, method, imports, indent) { let name = method.name; if (method.kind !== 'nextPageMethod') { name = method.naming.requestMethod; } for (const param of method.parameters) { if (param.location !== 'method' || !go.isRequiredParameter(param.style)) { continue; } imports.addForType(param.type); } const returns = ['*policy.Request', 'error']; let text = `${helpers.comment(name, '// ')} creates the ${method.name} request.\n`; text += `func ${getClientReceiverDefinition(method.receiver)} ${name}(${helpers.getCreateRequestParametersSig(method)}) (${returns.join(', ')}) {\n`; const hostParams = new Array(); for (const parameter of method.receiver.type.parameters) { if (parameter.kind === 'uriParam') { hostParams.push(parameter); } } let hostParam; if (azureARM) { hostParam = 'client.internal.Endpoint()'; } else if (method.receiver.type.instance?.kind === 'templatedHost') { imports.add('strings'); // we have a templated host text += `${indent.get()}host := "${method.receiver.type.instance.path}"\n`; // get all the host params on the client for (const hostParam of hostParams) { text += `${indent.get()}host = strings.ReplaceAll(host, "{${hostParam.uriPathSegment}}", ${helpers.formatValue(`client.${hostParam.name}`, hostParam.type, imports)})\n`; } // check for any method local host params for (const param of method.parameters) { if (param.location === 'method' && param.kind === 'uriParam') { text += `${indent.get()}host = strings.ReplaceAll(host, "{${param.uriPathSegment}}", ${helpers.formatValue(helpers.getParamName(param), param.type, imports)})\n`; } } hostParam = 'host'; } else if (hostParams.length === 1) { // simple parameterized host case hostParam = 'client.' + hostParams[0].name; } else { throw new CodegenError('InternalError', `no host or endpoint defined for method ${method.receiver.type.name}.${method.name}`); } const methodParamGroups = helpers.getMethodParamGroups(method); const hasPathParams = methodParamGroups.pathParams.length > 0; // storage needs the client.u to be the source-of-truth for the full path. // however, swagger requires that all operations specify a path, which is at odds with storage. // to work around this, storage specifies x-ms-path paths with path params but doesn't // actually reference the path params (i.e. no params with which to replace the tokens). // so, if a path contains tokens but there are no path params, skip emitting the path. const pathStr = method.httpPath; const pathContainsParms = pathStr.includes('{'); if (hasPathParams || (!pathContainsParms && pathStr.length > 1)) { // there are path params, or the path doesn't contain tokens and is not "/" so emit it text += `${indent.get()}urlPath := "${method.httpPath}"\n`; hostParam = `runtime.JoinPaths(${hostParam}, urlPath)`; } // helper to build nil checks for param groups const emitParamGroupCheck = function (param) { if (!param.group) { throw new CodegenError('InternalError', `emitParamGroupCheck called for ungrouped parameter ${param.name}`); } let client = ''; if (param.location === 'client') { client = 'client.'; } const paramGroupName = naming.uncapitalize(param.group.name); let optionalParamGroupCheck = `${client}${paramGroupName} != nil && `; if (param.group.required) { optionalParamGroupCheck = ''; } return `${indent.get()}if ${optionalParamGroupCheck}${client}${paramGroupName}.${naming.capitalize(param.name)} != nil {\n`; }; if (hasPathParams) { // swagger defines path params, emit path and replace tokens imports.add('strings'); // replace path parameters for (const pp of methodParamGroups.pathParams) { let paramValue; let optionalPathSep = false; if (pp.style === 'literal') { // literals are always scalar types and require no empty checks paramValue = helpers.formatParamValue(pp, imports, indent); } else if (pp.style === 'required' || pp.location === 'client') { // NOTE: we include client params here since they behave // like required params (i.e. not grouped). // emit check to ensure path param isn't a