@autorest/go
Version:
AutoRest Go Generator
988 lines • 98.2 kB
JavaScript
/*---------------------------------------------------------------------------------------------
* 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