private adaptMethodParameters()

in packages/typespec-go/src/tcgcadapter/clients.ts [294:463]


  private adaptMethodParameters(sdkMethod: tcgc.SdkServiceMethod<tcgc.SdkHttpOperation>, method: go.Method | go.NextPageMethod): Map<tcgc.SdkHttpParameter, Array<go.Parameter>> {
    const paramMapping = new Map<tcgc.SdkHttpParameter, Array<go.Parameter>>();

    let optionalGroup: go.ParameterGroup | undefined;
    if (go.isMethod(method)) {
      // NextPageMethods don't have optional params
      optionalGroup = method.optionalParamsGroup;
      if (go.isLROMethod(method)) {
        optionalGroup.params.push(new go.ResumeTokenParameter());
      }
    }

    // stuff all of the operation parameters into one array for easy traversal
    type OperationParamType = tcgc.SdkBodyParameter | tcgc.SdkHeaderParameter | tcgc.SdkPathParameter | tcgc.SdkQueryParameter | tcgc.SdkCookieParameter;
    const allOpParams = new Array<OperationParamType>();
    allOpParams.push(...sdkMethod.operation.parameters);
    if (sdkMethod.operation.bodyParam) {
      allOpParams.push(sdkMethod.operation.bodyParam);
    }

    // we must enumerate parameters, not operation.parameters, as it
    // contains the params in tsp order as well as any spread params.
    for (const param of sdkMethod.parameters) {
      // we need to translate from the method param to its underlying operation param.
      // most params have a one-to-one mapping. however, for spread params, there will
      // be a many-to-one mapping. i.e. multiple params will map to the same underlying
      // operation param. each param corresponds to a field within the operation param.
      let opParam = values(allOpParams).where((opParam: OperationParamType) => {
        return values(opParam.correspondingMethodParams).where((methodParam: tcgc.SdkModelPropertyType) => {
          if (param.type.kind === 'model') {
            for (const property of param.type.properties) {
              if (property === methodParam) {
                return true;
              }
            }
          }
          return methodParam === param;
        }).any();
      }).first();

      // special handling for constants that used in path, this will not be in operation parameters since it has been resolved in the url
      if (!opParam && param.type.kind === 'constant') {
        continue;
      }

      // special handling for `@bodyRoot`/`@body` on model param's property
      if (!opParam && param.type.kind === 'model') {
        for (const property of param.type.properties) {
          opParam = values(allOpParams).where((opParam: OperationParamType) => {
            return values(opParam.correspondingMethodParams).where((methodParam: tcgc.SdkModelPropertyType) => {
              return methodParam === property;
            }).any();
          }).first();
          if (opParam) {
            break;
          }
        }
      }

      if (!opParam) {
        throw new AdapterError('InternalError', `didn't find operation parameter for method ${sdkMethod.name} parameter ${param.name}`, sdkMethod.__raw?.node ?? NoTarget);
      }

      if (opParam.kind === 'header' && opParam.serializedName.match(/^content-type$/i) && param.type.kind === 'constant') {
        // if the body param is optional, then the content-type param is also optional.
        // for optional constants, this has the side effect of the param being treated like
        // a flag which isn't what we want. so, we mark it as required. we ONLY do this
        // if the content-type is a constant (i.e. literal value).
        // the content-type will be conditionally set based on the requiredness of the body.
        opParam.optional = false;
      }

      let adaptedParam: go.Parameter;
      if (opParam.kind === 'body' && opParam.type.kind === 'model' && opParam.type.kind !== param.type.kind) {
        const paramKind = this.adaptParameterKind(param);
        const byVal = isTypePassedByValue(param.type);
        const contentType = this.adaptContentType(opParam.defaultContentType);
        const getSerializedNameFromProperty = function(property: tcgc.SdkBodyModelPropertyType): string | undefined {
          if (contentType === 'JSON') {
            return property.serializationOptions.json?.name;
          }
          if (contentType === 'XML') {
            return property.serializationOptions.xml?.name;
          }
          if (contentType === 'binary') {
            return property.serializationOptions.multipart?.name;
          }
          return undefined;
        };
        switch (contentType) {
          case 'JSON':
          case 'XML': {
            // find the corresponding field within the model param so we can get the serialized name
            let serializedName: string | undefined;
            for (const property of opParam.type.properties) {
              if (property.name === param.name) {
                serializedName = getSerializedNameFromProperty(<tcgc.SdkBodyModelPropertyType>property);
                break;
              }
            }
            if (!serializedName) {
              throw new AdapterError('InternalError', `didn't find body model property for spread parameter ${param.name}`, param.__raw?.node ?? NoTarget);
            }
            adaptedParam = new go.PartialBodyParameter(param.name, serializedName, contentType, this.ta.getPossibleType(param.type, true, true), paramKind, byVal);
            break;
          }
          case 'binary':
            if (opParam.defaultContentType.match(/multipart/i)) {
              adaptedParam = new go.MultipartFormBodyParameter(param.name, this.ta.getReadSeekCloser(false), paramKind, byVal);
            } else {
              adaptedParam = new go.BodyParameter(param.name, contentType, `"${opParam.defaultContentType}"`, this.ta.getReadSeekCloser(false), paramKind, byVal);
            }
            break;
          default:
            throw new AdapterError('UnsupportedTsp', `unsupported spread param content type ${contentType}`, opParam.__raw?.node ?? NoTarget);
        }
      } else {
        adaptedParam = this.adaptMethodParameter(opParam);
      }

      adaptedParam.docs.summary = param.summary;
      adaptedParam.docs.description = param.doc;
      method.parameters.push(adaptedParam);
      if (!paramMapping.has(opParam)) {
        paramMapping.set(opParam, new Array<go.Parameter>());
      }
      paramMapping.get(opParam)?.push(adaptedParam);

      if (adaptedParam.kind !== 'required' && adaptedParam.kind !== 'literal') {
        // add optional method param to the options param group
        if (!optionalGroup) {
          throw new AdapterError('InternalError', `optional parameter ${param.name} has no optional parameter group`, param.__raw?.node ?? NoTarget);
        }
        adaptedParam.group = optionalGroup;
        optionalGroup.params.push(adaptedParam);
      }
    }

    // client params aren't included in method.parameters so
    // look for them in the operation parameters.
    for (const opParam of allOpParams) {
      if (opParam.onClient) {
        const adaptedParam = this.adaptMethodParameter(opParam);
        adaptedParam.docs.summary = opParam.summary;
        adaptedParam.docs.description = opParam.doc;
        method.parameters.unshift(adaptedParam);
        if (!paramMapping.has(opParam)) {
          paramMapping.set(opParam, new Array<go.Parameter>());
        }
        paramMapping.get(opParam)?.push(adaptedParam);

        // if the adapted client param is a literal then don't add it to
        // the array of client params as it's not a formal parameter.
        if (go.isLiteralParameter(adaptedParam)) {
          continue;
        }

        // we must check via param name and not reference equality. this is because a client param
        // can be used in multiple ways. e.g. a client param "apiVersion" that's used as a path param
        // in one method and a query param in another.
        if (!method.client.parameters.find((v: go.Parameter) => {
          return v.name === adaptedParam.name;
        })) {
          method.client.parameters.push(adaptedParam);
        }
      }
    }

    return paramMapping;
  }