UNPKG

@autorest/go

Version:
959 lines 61.2 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 { capitalize, KnownMediaType, serialize, uncapitalize } from '@azure-tools/codegen'; import { startSession } from '@autorest/extension-base'; import * as m4 from '@autorest/codemodel'; import { clone, items, values } from '@azure-tools/linq'; import { createOptionsTypeDescription, createResponseEnvelopeDescription } from '../../../naming.go/src/naming.js'; import * as helpers from './helpers.js'; import { namer, protocolMethods } from './namer.js'; import { fromString } from 'html-to-text'; import showdown from 'showdown'; import { fileURLToPath } from 'url'; const { Converter } = showdown; // The transformer adds Go-specific information to the code model. export async function transformM4(host) { const debug = await host.getValue('debug') || false; try { const session = await startSession(host, m4.codeModelSchema); // run the namer first, so that any transformations are applied on proper names await namer(session); await process(session); await labelUnreferencedTypes(session); // output the model to the pipeline host.writeFile({ filename: 'code-model-v4-transform.yaml', content: serialize(session.model), artifactType: 'code-model-v4' }); } catch (E) { if (debug) { console.error(`${fileURLToPath(import.meta.url)} - FAILURE ${JSON.stringify(E)} ${E.stack}`); } throw E; } } async function process(session) { await processOperationRequests(session); processOperationResponses(session); // fix up dictionary element types (additional properties) // this must happen before processing objects as we depend on the // schema type being an actual Go type. for (const dictionary of values(session.model.schemas.dictionaries)) { dictionary.language.go.name = schemaTypeToGoType(session.model, dictionary, 'Property'); dictionary.language.go.elementIsPtr = !helpers.isTypePassedByValue(dictionary.elementType); if (dictionary.language.go.description) { dictionary.language.go.description = parseComments(dictionary.language.go.description); } } // fix up struct field types for (const obj of values(session.model.schemas.objects)) { if (obj.language.go.description) { obj.language.go.description = parseComments(obj.language.go.description); if (!obj.language.go.description.startsWith(obj.language.go.name)) { obj.language.go.description = `${obj.language.go.name} - ${obj.language.go.description}`; } } if (obj.discriminator) { // discriminators will contain the root type of each discriminated type hierarchy if (!session.model.language.go.discriminators) { session.model.language.go.discriminators = new Array(); } const defs = session.model.language.go.discriminators; const rootDiscriminator = getRootDiscriminator(obj); if (defs.indexOf(rootDiscriminator) < 0) { rootDiscriminator.language.go.rootDiscriminator = true; defs.push(rootDiscriminator); // fix up discriminator value to use the enum type if available const discriminatorEnums = getDiscriminatorEnums(rootDiscriminator); // for each child type in the hierarchy, fix up the discriminator value for (const child of values(rootDiscriminator.children?.all)) { const asObj = child; let discValue = getEnumForDiscriminatorValue(asObj.discriminatorValue, discriminatorEnums); if (!discValue) { discValue = quoteString(asObj.discriminatorValue); } asObj.discriminatorValue = discValue; } } } for (const prop of values(obj.properties)) { if (prop.language.go.description) { prop.language.go.description = parseComments(prop.language.go.description); } const details = prop.schema.language.go; details.name = `${schemaTypeToGoType(session.model, prop.schema, 'InBody')}`; prop.schema = substitueDiscriminator(prop); if (prop.schema.type === m4.SchemaType.Any || prop.schema.type === m4.SchemaType.AnyObject || (helpers.isObjectSchema(prop.schema) && prop.schema.discriminator)) { prop.language.go.byValue = true; } else if (prop.schema.type === m4.SchemaType.DateTime) { obj.language.go.needsDateTimeMarshalling = true; } else if (prop.schema.type === m4.SchemaType.Date) { obj.language.go.needsDateMarshalling = true; } else if (prop.schema.type === m4.SchemaType.UnixTime) { obj.language.go.needsUnixTimeMarshalling = true; } else if (prop.schema.type === m4.SchemaType.Dictionary && obj.language.go.marshallingFormat === 'xml') { // mark that we need custom XML unmarshalling for a dictionary prop.language.go.needsXMLDictionaryUnmarshalling = true; session.model.language.go.needsXMLDictionaryUnmarshalling = true; } else if (prop.schema.type === m4.SchemaType.ByteArray) { prop.language.go.byValue = true; obj.language.go.byteArrayFormat = prop.schema.format; } if (prop.schema.type === m4.SchemaType.Array || prop.schema.type === m4.SchemaType.Dictionary) { obj.language.go.hasArrayMap = true; prop.language.go.byValue = true; if (prop.schema.type !== m4.SchemaType.Dictionary && obj.language.go.marshallingFormat === 'xml') { prop.language.go.needsXMLArrayMarshalling = true; } } } if (!obj.language.go.marshallingFormat) { // TODO: workaround due to https://github.com/Azure/autorest.go/issues/412 // this type isn't used as a parameter/return value so it has no marshalling format. // AutoRest doesn't make the global configuration available at present so hard-code // the format to JSON as the vast majority of specs use JSON. obj.language.go.marshallingFormat = 'json'; } const addPropsSchema = helpers.hasAdditionalProperties(obj); if (addPropsSchema) { // add an 'AdditionalProperties' field to the type const addProps = newProperty('AdditionalProperties', 'OPTIONAL; Contains additional key/value pairs not defined in the schema.', addPropsSchema); addProps.language.go.isAdditionalProperties = true; addProps.language.go.byValue = true; obj.properties?.push(addProps); } } // fix up enum types for (const choice of values(session.model.schemas.choices)) { choice.choiceType.language.go.name = schemaTypeToGoType(session.model, choice.choiceType, 'Property'); if (choice.language.go.description) { choice.language.go.description = parseComments(choice.language.go.description); } } for (const choice of values(session.model.schemas.sealedChoices)) { choice.choiceType.language.go.name = schemaTypeToGoType(session.model, choice.choiceType, 'Property'); if (choice.language.go.description) { choice.language.go.description = parseComments(choice.language.go.description); } } } // used by getDiscriminatorSchema and substitueDiscriminator const discriminatorSchemas = new Map(); // creates/gets an ObjectSchema for a discriminated type. // NOTE: assumes schema is a discriminator. function getDiscriminatorSchema(schema) { const discriminatorInterface = schema.language.go.discriminatorInterface; if (!discriminatorSchemas.has(discriminatorInterface)) { const discriminatorSchema = new m4.ObjectSchema(discriminatorInterface, 'discriminated type'); discriminatorSchema.language.go = discriminatorSchema.language.default; discriminatorSchema.language.go.discriminatorInterface = discriminatorInterface; // copy over fields from the original discriminatorSchema.discriminator = schema.discriminator; discriminatorSchema.children = schema.children; discriminatorSchema.parents = schema.parents; discriminatorSchemas.set(discriminatorInterface, discriminatorSchema); } return discriminatorSchemas.get(discriminatorInterface); } // replaces item's schema with the appropriate discriminator schema as required function substitueDiscriminator(item) { if (item.schema.type === m4.SchemaType.Object && item.schema.language.go.discriminatorInterface) { item.language.go.byValue = true; return getDiscriminatorSchema(item.schema); } else if (helpers.isArraySchema(item.schema) || helpers.isDictionarySchema(item.schema)) { const leafElementSchema = helpers.recursiveUnwrapArrayDictionary(item.schema); if (leafElementSchema.type === m4.SchemaType.Object && leafElementSchema.language.go.discriminatorInterface) { return recursiveSubstitueDiscriminator(item.schema); } } // not a discriminator return item.schema; } // constructs a new schema for arrays/maps with a leaf element that's a discriminator // NOTE: assumes that the leaf element type is a discriminated type // e.g. []map[string]DiscriminatorInterface function recursiveSubstitueDiscriminator(item) { const strings = recursiveBuildDiscriminatorStrings(item); let discriminatorSchema = discriminatorSchemas.get(strings.Name); if (discriminatorSchema) { return discriminatorSchema; } if (helpers.isArraySchema(item)) { discriminatorSchema = new m4.ArraySchema(strings.Name, strings.Desc, recursiveSubstitueDiscriminator(item.elementType)); discriminatorSchema.language.go = discriminatorSchema.language.default; discriminatorSchema.language.go.elementIsPtr = false; discriminatorSchemas.set(strings.Name, discriminatorSchema); return discriminatorSchema; } else if (helpers.isDictionarySchema(item)) { discriminatorSchema = new m4.DictionarySchema(strings.Name, strings.Desc, recursiveSubstitueDiscriminator(item.elementType)); discriminatorSchema.language.go = discriminatorSchema.language.default; discriminatorSchema.language.go.elementIsPtr = false; discriminatorSchemas.set(strings.Name, discriminatorSchema); return discriminatorSchema; } return getDiscriminatorSchema(item); } // constructs the name and description for arrays/maps with a leaf element that's a discriminator // NOTE: assumes that the leaf element type is a discriminated type // Name: []map[string]DiscriminatorInterface // Desc: slice of map of discriminators function recursiveBuildDiscriminatorStrings(item) { if (helpers.isArraySchema(item)) { const strings = recursiveBuildDiscriminatorStrings(item.elementType); return { Name: `[]${strings.Name}`, Desc: `array of ${strings.Desc}` }; } else if (helpers.isDictionarySchema(item)) { const strings = recursiveBuildDiscriminatorStrings(item.elementType); return { Name: `map[string]${strings.Name}`, Desc: `map of ${strings.Desc}` }; } return { Name: getDiscriminatorSchema(item).language.go.discriminatorInterface, Desc: 'discriminators' }; } const dictionaryElementAnySchema = new m4.AnySchema('any schema for maps'); dictionaryElementAnySchema.language.go = dictionaryElementAnySchema.language.default; dictionaryElementAnySchema.language.go.name = 'any'; function schemaTypeToGoType(codeModel, schema, type) { const rawJSONAsBytes = codeModel.language.go.rawJSONAsBytes; switch (schema.type) { case m4.SchemaType.Any: if (rawJSONAsBytes) { schema.language.go.rawJSONAsBytes = rawJSONAsBytes; return '[]byte'; } return 'any'; case m4.SchemaType.AnyObject: if (rawJSONAsBytes) { schema.language.go.rawJSONAsBytes = rawJSONAsBytes; return '[]byte'; } return 'map[string]any'; case m4.SchemaType.ArmId: return 'string'; case m4.SchemaType.Array: { const arraySchema = schema; const arrayElem = arraySchema.elementType; if (rawJSONAsBytes && (arrayElem.type === m4.SchemaType.Any || arrayElem.type === m4.SchemaType.AnyObject)) { schema.language.go.rawJSONAsBytes = rawJSONAsBytes; // propagate the setting to the element type arrayElem.language.go.rawJSONAsBytes = rawJSONAsBytes; return '[][]byte'; } arraySchema.language.go.elementIsPtr = !helpers.isTypePassedByValue(arrayElem) && !codeModel.language.go.sliceElementsByValue; // passing nil for array elements in headers, paths, and query params // isn't very useful as we'd just skip nil entries. so disable it. if (type !== 'Property' && type !== 'InBody') { arraySchema.language.go.elementIsPtr = false; } arrayElem.language.go.name = schemaTypeToGoType(codeModel, arrayElem, type); if (arraySchema.language.go.elementIsPtr) { return `[]*${arrayElem.language.go.name}`; } return `[]${arrayElem.language.go.name}`; } case m4.SchemaType.Binary: return 'io.ReadSeekCloser'; case m4.SchemaType.Boolean: return 'bool'; case m4.SchemaType.ByteArray: return '[]byte'; case m4.SchemaType.Char: return 'rune'; case m4.SchemaType.Constant: { const constSchema = schema; constSchema.valueType.language.go.name = schemaTypeToGoType(codeModel, constSchema.valueType, type); return constSchema.valueType.language.go.name; } case m4.SchemaType.DateTime: { const dateTime = schema; if (dateTime.format === 'date-time-rfc1123') { schema.language.go.internalTimeType = 'dateTimeRFC1123'; } else { schema.language.go.internalTimeType = 'dateTimeRFC3339'; } if (type === 'InBody') { // add a marker to the code model indicating that we need // to include support for marshalling/unmarshalling time. // header/query param values are parsed separately so they // don't need the custom time types. if (dateTime.format === 'date-time-rfc1123') { codeModel.language.go.generateDateTimeRFC1123Helper = true; } else { codeModel.language.go.generateDateTimeRFC3339Helper = true; } } return 'time.Time'; } case m4.SchemaType.UnixTime: // unix time always requires the helper time type codeModel.language.go.generateUnixTimeHelper = true; schema.language.go.internalTimeType = 'timeUnix'; return 'time.Time'; case m4.SchemaType.Dictionary: { const dictSchema = schema; const dictElem = dictSchema.elementType; if (rawJSONAsBytes && (dictElem.type === m4.SchemaType.Any || dictElem.type === m4.SchemaType.AnyObject)) { dictSchema.elementType = dictionaryElementAnySchema; return `map[string]${dictionaryElementAnySchema.language.go.name}`; } dictSchema.language.go.elementIsPtr = !helpers.isTypePassedByValue(dictSchema.elementType); dictElem.language.go.name = schemaTypeToGoType(codeModel, dictElem, type); if (dictSchema.language.go.elementIsPtr) { return `map[string]*${dictElem.language.go.name}`; } return `map[string]${dictElem.language.go.name}`; } case m4.SchemaType.Integer: if (schema.precision === 32) { return 'int32'; } return 'int64'; case m4.SchemaType.Number: if (schema.precision === 32) { return 'float32'; } return 'float64'; case m4.SchemaType.Credential: case m4.SchemaType.Duration: case m4.SchemaType.ODataQuery: case m4.SchemaType.String: case m4.SchemaType.Uuid: case m4.SchemaType.Uri: return 'string'; case m4.SchemaType.Date: schema.language.go.internalTimeType = 'dateType'; if (type === 'InBody') { codeModel.language.go.generateDateHelper = true; } return 'time.Time'; case m4.SchemaType.Time: schema.language.go.internalTimeType = 'timeRFC3339'; if (type === 'InBody') { codeModel.language.go.generateTimeRFC3339Helper = true; } return 'time.Time'; case m4.SchemaType.Choice: case m4.SchemaType.SealedChoice: case m4.SchemaType.Object: return schema.language.go.name; default: throw new Error(`unhandled schema type ${schema.type}`); } } function recursiveAddMarshallingFormat(schema, marshallingFormat) { // only recurse if the schema isn't a primitive type const shouldRecurse = function (schema) { return schema.type === m4.SchemaType.Array || schema.type === m4.SchemaType.Dictionary || schema.type === m4.SchemaType.Object; }; if (schema.language.go.marshallingFormat) { // this schema has already been processed, don't do it again return; } schema.language.go.marshallingFormat = marshallingFormat; switch (schema.type) { case m4.SchemaType.Array: { const arraySchema = schema; if (shouldRecurse(arraySchema.elementType)) { recursiveAddMarshallingFormat(arraySchema.elementType, marshallingFormat); } break; } case m4.SchemaType.Dictionary: { const dictSchema = schema; if (shouldRecurse(dictSchema.elementType)) { recursiveAddMarshallingFormat(dictSchema.elementType, marshallingFormat); } break; } case m4.SchemaType.Object: { const os = schema; for (const prop of values(os.properties)) { if (shouldRecurse(prop.schema)) { recursiveAddMarshallingFormat(prop.schema, marshallingFormat); } } // if this is a discriminated type, update children and parents for (const child of values(os.children?.all)) { recursiveAddMarshallingFormat(child, marshallingFormat); } for (const parent of values(os.parents?.all)) { recursiveAddMarshallingFormat(parent, marshallingFormat); } break; } } } // we will transform operation request parameter schema types to Go types async function processOperationRequests(session) { // pre-process multi-request operations as it can add operations to the operations // collection, and iterating over a modified collection yeilds incorrect results for (const group of values(session.model.operationGroups)) { for (const op of values(group.operations).toArray()) { if (op.language.go.description) { op.language.go.description = parseComments(op.language.go.description); } const normalizeOperationName = await session.getValue('normalize-operation-name', false); // previous operation naming logic: keep original name if only one body type, and add suffix for operation with non-binary body type if more than one body type // new normalized operation naming logic: add suffix for operation with unstructured body type and keep original name for operation with structured body type if (!normalizeOperationName) { if (op.requests.length > 1) { // for the non-binary media types we create a new method with the // media type name as a suffix, e.g. FooAPIWithJSON() separateOperationByRequestsProtocol(group, op, [KnownMediaType.Binary]); } } else { // add suffix to binary/text, suppose there will be only one structured media type separateOperationByRequestsProtocol(group, op, [KnownMediaType.Json, KnownMediaType.Xml, KnownMediaType.Form, KnownMediaType.Multipart]); } if (!group.language.go.host) { // all operations/groups have the same host, so just grab the first one group.language.go.host = op.requests?.[0].protocol.http?.uri; } } } // track any client-level parameterized host params. const hostParams = new Array(); // track any parameter groups and/or optional parameters const paramGroups = new Map(); const singleClient = session.model.language.go.singleClient; if (singleClient && session.model.operationGroups.length > 1) { throw new Error('single-client cannot be enabled when there are multiple clients'); } for (const group of values(session.model.operationGroups)) { for (const op of values(group.operations)) { if (op.language.go.description) { op.language.go.description = parseComments(op.language.go.description); } if (op.requests[0].protocol.http.headers) { for (const header of values(op.requests[0].protocol.http.headers)) { const head = header; head.schema.language.go.name = schemaTypeToGoType(session.model, head.schema, 'Property'); } } const opName = helpers.isLROOperation(op) ? 'Begin' + op.language.go.name : op.language.go.name; const params = values(helpers.aggregateParameters(op)); // create an optional params struct even if the operation contains no optional params. // this provides version resiliency in case optional params are added in the future. // don't do this for paging next link operation as this isn't part of the public API if (!op.language.go.paging || !op.language.go.paging.isNextOp) { // create a type named <OperationGroup><Operation>Options // if single-client is enabled, omit the <OperationGroup> prefix let clientPrefix = capitalize(group.language.go.clientName); if (singleClient) { clientPrefix = ''; } const optionalParamsGroupName = `${clientPrefix}${opName}Options`; const desc = createOptionsTypeDescription(optionalParamsGroupName, `${group.language.go.clientName}.${helpers.isPageableOperation(op) && !helpers.isLROOperation(op) ? `New${opName}Pager` : opName}`); const gp = createGroupProperty(optionalParamsGroupName, desc, false); gp.language.go.name = 'options'; // if there's an existing parameter with the name options then pick something else for (const param of params) { if (param.language.go.name === gp.language.go.name) { gp.language.go.name = 'opts'; break; } } gp.required = false; paramGroups.set(optionalParamsGroupName, gp); // associate the param group with the operation op.language.go.optionalParamGroup = gp; } for (const param of params) { if (param.language.go.description) { param.language.go.description = parseComments(param.language.go.description); } if (param.clientDefaultValue && param.implementation === m4.ImplementationLocation.Method) { // we treat method params with a client-side default as optional // since if you don't specify a value, a default is sent and the // zero-value is ambiguous. // NOTE: the assumption for client parameters with client-side defaults is that // the proper default values are being set in the client (hand-written) constructor. param.required = false; } if (!param.required && param.schema.type === m4.SchemaType.Constant && !param.language.go.amendedDesc) { if (param.language.go.description) { param.language.go.description += '. '; } param.language.go.description += `Specifying any value will set the value to ${param.schema.value.value}.`; param.language.go.amendedDesc = true; } // this is to work around M4 bug #202 // replace the duplicate operation entry in nextLinkOperation with // the one from our operation group so that things like parameter // groups/types etc are consistent. if (op.language.go.paging && op.language.go.paging.nextLinkOperation) { const dupeOp = op.language.go.paging.nextLinkOperation; for (const internalOp of values(group.operations)) { if (internalOp.language.default.name === dupeOp.language.default.name) { op.language.go.paging.nextLinkOperation = internalOp; break; } } for (const internalOp of values(group.operations)) { if (internalOp.language.go.name === dupeOp.language.default.name) { if (!internalOp.language.go.paging.isNextOp) { internalOp.language.go.paging.isNextOp = true; internalOp.language.go.name = `${uncapitalize(internalOp.language.go.name)}CreateRequest`; } break; } } } let paramType; switch (param.protocol.http?.in) { case 'body': paramType = 'InBody'; break; case 'header': paramType = 'HeaderParam'; break; case 'path': paramType = 'PathParam'; break; case 'query': paramType = 'QueryParam'; break; default: paramType = 'Property'; } param.schema.language.go.name = schemaTypeToGoType(session.model, param.schema, paramType); if (helpers.isTypePassedByValue(param.schema)) { param.language.go.byValue = true; } param.schema = substitueDiscriminator(param); // check if this is a header collection if (param.extensions?.['x-ms-header-collection-prefix']) { param.schema.language.go.headerCollectionPrefix = param.extensions['x-ms-header-collection-prefix']; } if (param.implementation === m4.ImplementationLocation.Client && (param.schema.type !== m4.SchemaType.Constant || !param.required)) { if (param.language.default.name === '$host' && session.model.language.go.azureARM) { // for ARM, the host is handled by azcore/arm.Client so we can skip it continue; } if (param.protocol.http.in === 'uri') { // this is a parameterized host param. // use the param name to avoid reference equality checks. if (!values(hostParams).where(p => p.language.go.name === param.language.go.name).any()) { hostParams.push(param); } // we special-case fully templated host param, e.g. {endpoint} // as there's no need to do a find/replace in this case, we'd // just directly use the endpoint param value. if (param.language.default.name !== '$host' && !group.language.go.host.match(/^\{\w+\}$/)) { group.language.go.complexHostParams = true; } continue; } // add global param info to the operation group if (group.language.go.clientParams === undefined) { group.language.go.clientParams = new Array(); } const clientParams = group.language.go.clientParams; // check if this global param has already been added if (values(clientParams).where(cp => cp.language.go.name === param.language.go.name).any()) { continue; } clientParams.push(param); } else if (param.implementation === m4.ImplementationLocation.Method && param.protocol.http.in === 'uri') { // at least one method contains a parameterized host param, bye-bye simple case group.language.go.complexHostParams = true; } // check for grouping if (param.extensions?.['x-ms-parameter-grouping'] && session.model.language.go.groupParameters) { // this param belongs to a param group, init name with default // if single-client is enabled, omit the <OperationGroup> prefix let clientPrefix = capitalize(group.language.go.clientName); if (singleClient) { clientPrefix = ''; } let paramGroupName = `${clientPrefix}${opName}Parameters`; if (param.extensions['x-ms-parameter-grouping'].name) { // use the specified name paramGroupName = param.extensions['x-ms-parameter-grouping'].name; } else if (param.extensions['x-ms-parameter-grouping'].postfix) { // use the suffix paramGroupName = `${clientPrefix}${opName}${param.extensions['x-ms-parameter-grouping'].postfix}`; } // create group entry and add the param to it if (!paramGroups.has(paramGroupName)) { let subtext = `.${opName} method`; let groupedClientParams = false; if (param.implementation === m4.ImplementationLocation.Client) { subtext = ' client'; groupedClientParams = true; } const desc = `${paramGroupName} contains a group of parameters for the ${group.language.go.clientName}${subtext}.`; const paramGroup = createGroupProperty(paramGroupName, desc, groupedClientParams); paramGroups.set(paramGroupName, paramGroup); } // associate the group with the param const paramGroup = paramGroups.get(paramGroupName); param.language.go.paramGroup = paramGroup; // check for a duplicate, if it has the same schema then skip it const dupe = values(paramGroup.originalParameter).first((each) => { return each.language.go.name === param.language.go.name; }); if (!dupe) { paramGroup.originalParameter.push(param); if (param.required) { // mark the group as required if at least one param in the group is required paramGroup.required = true; } } else if (dupe.schema !== param.schema) { throw new Error(`parameter group ${paramGroupName} contains overlapping parameters with different schemas`); } // check for groupings of client and method params for (const otherParam of values(paramGroup?.originalParameter)) { if (otherParam.implementation !== param.implementation) { throw new Error(`parameter group ${paramGroupName} contains client and method parameters`); } } } else if (param.implementation === m4.ImplementationLocation.Method && param.required !== true) { // include all non-required method params in the optional values struct. op.language.go.optionalParamGroup.originalParameter.push(param); // associate the group with the param param.language.go.paramGroup = op.language.go.optionalParamGroup; } } if (helpers.isLROOperation(op)) { // add the ResumeToken to the optional params type const tokenParam = newParameter('ResumeToken', '', newString('string', '')); tokenParam.language.go.byValue = true; tokenParam.language.go.isResumeToken = true; tokenParam.required = false; op.parameters?.push(tokenParam); tokenParam.language.go.paramGroup = op.language.go.optionalParamGroup; op.language.go.optionalParamGroup.originalParameter.push(tokenParam); } // recursively add the marshalling format to the body param if applicable const marshallingFormat = getMarshallingFormat(op.requests[0].protocol); if (marshallingFormat !== 'na') { const bodyParam = values(helpers.aggregateParameters(op)).where((each) => { return each.protocol.http?.in === 'body'; }).first(); if (bodyParam) { recursiveAddMarshallingFormat(bodyParam.schema, marshallingFormat); if (marshallingFormat === 'xml' && bodyParam.schema.serialization?.xml?.name) { // mark that this parameter type will need a custom marshaller to handle the XML name bodyParam.schema.language.go.xmlWrapperName = bodyParam.schema.serialization?.xml?.name; } else if (marshallingFormat === 'json' && op.requests[0].protocol.http.method === 'patch') { // mark that this type will need a custom marshaller to handle JSON nulls bodyParam.schema.language.go.needsPatchMarshaller = true; } } } } if (hostParams.length > 0) { // attach host params to the operation group group.language.go.hostParams = hostParams; } else if (!session.model.language.go.azureARM) { // if there are no host params and this isn't Azure ARM, check for a swagger-provided host (e.g. test server) for (const param of values(session.model.globalParameters)) { if (param.language.default.name === '$host') { group.language.go.host = param.clientDefaultValue; session.model.language.go.host = group.language.go.host; break; } } } } // emit any param groups if (paramGroups.size > 0) { if (!session.model.language.go.parameterGroups) { session.model.language.go.parameterGroups = new Array(); } const pg = session.model.language.go.parameterGroups; for (const items of paramGroups.entries()) { pg.push(items[1]); } } } function createGroupProperty(name, description, groupedClientParams) { const schema = new m4.ObjectSchema(name, description); schema.language.go = schema.language.default; const gp = new m4.GroupProperty(name, description, schema); gp.language.go = gp.language.default; if (groupedClientParams) { gp.language.go.groupedClientParams = true; } return gp; } function processOperationResponses(session) { if (session.model.language.go.responseEnvelopes === undefined) { session.model.language.go.responseEnvelopes = new Array(); } for (const group of values(session.model.operationGroups)) { for (const op of values(group.operations)) { // recursively add the marshalling format to the responses if applicable. // also remove any HTTP redirects from the list of responses. const filtered = new Array(); for (const resp of values(op.responses)) { if (skipRedirectStatusCode(op.requests[0].protocol.http.method, resp)) { // redirects are transient status codes, they aren't actually returned continue; } if (helpers.isSchemaResponse(resp)) { if (resp.schema.type === m4.SchemaType.Binary) { // don't create response envelopes for binary responses. // callers read directly from the *http.Response.Body continue; } resp.schema.language.go.name = schemaTypeToGoType(session.model, resp.schema, 'InBody'); } const marshallingFormat = getMarshallingFormat(resp.protocol); if (marshallingFormat !== 'na' && helpers.isSchemaResponse(resp)) { recursiveAddMarshallingFormat(resp.schema, marshallingFormat); } // fix up schema types for header responses const httpResponse = resp.protocol.http; for (const header of values(httpResponse.headers)) { header.schema.language.go.name = schemaTypeToGoType(session.model, header.schema, 'Property'); // check if this is a header collection if (header.extensions?.['x-ms-header-collection-prefix']) { header.schema.language.go.headerCollectionPrefix = header.extensions['x-ms-header-collection-prefix']; } } filtered.push(resp); if (resp.language.go.description) { resp.language.go.description = parseComments(resp.language.go.description); } } // replace with the filtered list if applicable if (filtered.length === 0) { // handling of operations with no responses expects an undefined list, not an empty one op.responses = undefined; } else if (op.responses?.length !== filtered.length) { op.responses = filtered; } createResponseEnvelope(session.model, group, op); } } } // returns true if the specified status code is an automatic HTTP redirect. // certain redirects are automatically handled by the HTTP stack and thus are // transient so they are never actually returned to the caller. we skip them // so they aren't included in the potential result set of an operation. function skipRedirectStatusCode(verb, resp) { const statusCodes = resp.protocol.http.statusCodes; if (statusCodes.length > 1) { return false; } // taken from src/net/http/client.go in the gostdlib switch (statusCodes[0]) { case '301': case '302': case '303': if (verb === 'get' || verb === 'head') { return true; } break; case '307': case '308': return true; } return false; } // the name of the struct field for scalar responses (int, string, etc) const scalarResponsePropName = 'Value'; // creates the response envelope type to be returned from an operation and updates the operation. function createResponseEnvelope(codeModel, group, op) { // create the `type <type>Response struct` response // aggregate headers from all responses as all of them will go into the same result envelope const headers = new Map(); // skip adding headers for LROs for now, this is to avoid adding // any LRO-specific polling headers. // TODO: maybe switch to skip specific polling ones in the future? if (!helpers.isLROOperation(op)) { for (const resp of values(op.responses)) { // check if the response is expecting information from headers for (const header of values(resp.protocol.http.headers)) { const head = header; // convert each header to a property and append it to the response properties list const name = head.language.go.name; if (!headers.has(name)) { const description = `${name} contains the information returned from the ${head.header} header response.`; headers.set(name, { ...head, description: description }); } } } } // contains all the response envelopes const responseEnvelopes = codeModel.language.go.responseEnvelopes; // first create the response envelope, each operation gets one // if single-client is enabled, omit the <OperationGroup> prefix let clientPrefix = capitalize(group.language.go.clientName); if (codeModel.language.go.singleClient) { clientPrefix = ''; } const respEnvName = ensureUniqueModelName(codeModel, `${clientPrefix}${op.language.go.name}Response`, 'Envelope'); const opName = helpers.isLROOperation(op) ? 'Begin' + op.language.go.name : op.language.go.name; const respEnv = newObject(respEnvName, createResponseEnvelopeDescription(respEnvName, `${group.language.go.clientName}.${helpers.isPageableOperation(op) && !helpers.isLROOperation(op) ? `New${opName}Pager` : opName}`)); respEnv.language.go.responseType = true; respEnv.properties = new Array(); responseEnvelopes.push(respEnv); op.language.go.responseEnv = respEnv; if (helpers.isLROOperation(op)) { respEnv.language.go.forLRO = true; } // add any headers to the response for (const item of items(headers)) { const prop = newRespProperty(item.key, item.value.description, item.value.schema, false); // propagate any extensions so we can access them through the property prop.extensions = item.value.extensions; prop.language.go.fromHeader = item.value.header; respEnv.properties.push(prop); } // now create the result field if (codeModel.language.go.headAsBoolean && op.requests[0].protocol.http.method === 'head') { op.language.go.headAsBoolean = true; const successProp = newProperty('Success', 'Success indicates if the operation succeeded or failed.', newBoolean('bool', 'bool response')); successProp.language.go.byValue = true; respEnv.properties.push(successProp); respEnv.language.go.resultProp = successProp; return; } if (helpers.isMultiRespOperation(op)) { const resultTypes = new Array(); for (const response of values(op.responses)) { // the operation might contain a mix of schemas and non-schema responses. // we only care about the ones that return a schema. if (helpers.isSchemaResponse(response)) { resultTypes.push(response.schema.language.go.name); } } const resultProp = newRespProperty('Value', `Possible types are ${resultTypes.join(', ')}\n`, newAny('multi-response value'), true); respEnv.properties.push(resultProp); respEnv.language.go.resultProp = resultProp; return; } const response = helpers.getSchemaResponse(op); // if the response defines a schema then add it to the response envelope if (response) { const rawJSONAsBytes = codeModel.language.go.rawJSONAsBytes; // propagate marshalling format to the response envelope respEnv.language.go.marshallingFormat = response.schema.language.go.marshallingFormat; // for operations that return scalar types we use a fixed field name let propName = scalarResponsePropName; if (response.schema.type === m4.SchemaType.Object) { // for object types use the type's name as the field name propName = response.schema.language.go.name; } else if (response.schema.type === m4.SchemaType.Array) { // for array types use the element type's name propName = recursiveTypeName(response.schema); } else if (rawJSONAsBytes && (response.schema.type === m4.SchemaType.Any || response.schema.type === m4.SchemaType.AnyObject)) { propName = 'RawJSON'; } else if (response.schema.type === m4.SchemaType.Any) { propName = 'Interface'; } else if (response.schema.type === m4.SchemaType.AnyObject) { propName = 'Object'; } if (response.schema.serialization?.xml && response.schema.serialization.xml.name) { // always prefer the XML name propName = capitalize(response.schema.serialization.xml.name); } // we want to pass integral types byref to maintain parity with struct fields const byValue = helpers.isTypePassedByValue(response.schema) || response.schema.type === m4.SchemaType.Object; const resultSchema = substitueDiscriminator(response); const resultProp = newRespProperty(propName, response.schema.language.go.description, resultSchema, byValue); if (resultSchema.language.go.discriminatorInterface) { // if the schema is a discriminator we need to flag this on the property itself. // this is so the correct unmarshaller is created for the response envelope. resultProp.isDiscriminator = true; } respEnv.properties.push(resultProp); respEnv.language.go.resultProp = resultProp; } else if (helpers.isBinaryResponseOperation(op)) { const binaryProp = newProperty('Body', 'Body contains the streaming response.', newBinary('binary response')); binaryProp.language.go.byValue = true; respEnv.properties.push(binaryProp); respEnv.language.go.resultProp = binaryProp; } if (respEnv.properties.length === 0) { // if we get here it means the operation doesn't return anything. we set // this to undefined to simplify detection of an empty response envelope respEnv.properties = undefined; } } // appends suffix to name if name is an existing model type function ensureUniqueModelName(codeModel, name, suffix) { for (const obj of values(codeModel.schemas.objects)) { if (obj.language.go.name === name) { return name + suffix; } } return name; } function newObject(name, desc) { const obj = new m4.ObjectSchema(name, desc); obj.language.go = obj.language.default; return obj; } function newAny(desc) { const any = new m4.AnySchema(desc); any.language.go = any.language.default; any.language.go.name = 'any'; return any; } function newBoolean(name, desc) { const bool = new m4.BooleanSchema(name, desc); bool.language.go = bool.language.default; bool.language.go.name = 'bool'; return bool; } function newBinary(desc) { const binary = new m4.BinarySchema(desc); binary.language.go = binary.language.default; binary.language.go.name = 'io.ReadCloser'; return binary; } function newString(name, desc) { const string = new m4.StringSchema(name, desc); string.language.go = string.language.default; string.language.go.name = 'string'; return string; } function newProperty(name, desc, schema) { const prop = new m4.Property(name, desc, schema); if (helpers.isObjectSchema(schema) && schema.discriminator) { prop.isDiscriminator = true; } prop.language.go = prop.language.default; return prop; } function newParameter(name, desc, schema) { const param = n