Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
changeKind: feature
packages:
- "@azure-tools/typespec-client-generator-core"
---

Add `methodParameterSegments` property to replace `correspondingMethodParams` with complete path segments.
1 change: 1 addition & 0 deletions packages/typespec-client-generator-core/src/clients.ts
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,7 @@ function getEndpointTypeFromSingleServer<
optional: false,
serializedName: "endpoint",
correspondingMethodParams: [],
methodParameterSegments: [],
type: getSdkBuiltInType(context, $(context.program).builtin.url),
isApiVersionParam: false,
apiVersions: context.getApiVersionsForType(client.__raw.type ?? client.__raw.service),
Expand Down
104 changes: 73 additions & 31 deletions packages/typespec-client-generator-core/src/http.ts
Original file line number Diff line number Diff line change
Expand Up @@ -230,21 +230,27 @@ function getSdkHttpParameters(
type,
optional: isHttpBodySpread(tspBody) ? false : (tspBody.property?.optional ?? false), // optional is always false for spread body
correspondingMethodParams,
methodParameterSegments: [],
crossLanguageDefinitionId: `${getCrossLanguageDefinitionId(context, httpOperation.operation)}.body`,
decorators: diagnostics.pipe(getTypeDecorators(context, tspBody.type)),
access: "public",
flatten: false,
};
}
if (retval.bodyParam) {
retval.bodyParam.correspondingMethodParams = diagnostics.pipe(
getCorrespondingMethodParams(
retval.bodyParam.methodParameterSegments = diagnostics.pipe(
getMethodParameterSegments(
context,
httpOperation.operation,
methodParameters,
retval.bodyParam,
),
);
// Derive correspondingMethodParams from methodParameterSegments (last element of each path)
// eslint-disable-next-line @typescript-eslint/no-deprecated
retval.bodyParam.correspondingMethodParams = retval.bodyParam.methodParameterSegments.map(
(segment) => segment[segment.length - 1],
);

addContentTypeInfoToBodyParam(context, httpOperation, retval.bodyParam);

Expand All @@ -253,6 +259,7 @@ function getSdkHttpParameters(
retval.bodyParam.type = diagnostics.pipe(
getStreamAsBytes(context, retval.bodyParam.type.__raw!),
);
// eslint-disable-next-line @typescript-eslint/no-deprecated
retval.bodyParam.correspondingMethodParams.map((p) => (p.type = retval.bodyParam!.type));
}
}
Expand All @@ -274,6 +281,7 @@ function getSdkHttpParameters(
kind: "header",
serializedName: "Content-Type",
correspondingMethodParams,
methodParameterSegments: [],
});
}
if (responseBody && !headerParams.some((h) => isAcceptHeader(h))) {
Expand All @@ -292,11 +300,17 @@ function getSdkHttpParameters(
kind: "header",
serializedName: "Accept",
correspondingMethodParams,
methodParameterSegments: [],
});
}
for (const param of retval.parameters) {
param.correspondingMethodParams = diagnostics.pipe(
getCorrespondingMethodParams(context, httpOperation.operation, methodParameters, param),
param.methodParameterSegments = diagnostics.pipe(
getMethodParameterSegments(context, httpOperation.operation, methodParameters, param),
);
// Derive correspondingMethodParams from methodParameterSegments (last element of each path)
// eslint-disable-next-line @typescript-eslint/no-deprecated
param.correspondingMethodParams = param.methodParameterSegments.map(
(segment) => segment[segment.length - 1],
);
}
return diagnostics.wrap(retval);
Expand Down Expand Up @@ -371,7 +385,9 @@ function addContentTypeInfoToBodyParam(
bodyParam.defaultContentType = defaultContentType;
diagnostics.pipe(addEncodeInfo(context, bodyParam.__raw!, bodyParam.type, defaultContentType));
// set the correct encode for body parameter of method according to the content-type
// eslint-disable-next-line @typescript-eslint/no-deprecated
if (bodyParam.correspondingMethodParams.length === 1) {
// eslint-disable-next-line @typescript-eslint/no-deprecated
const methodBodyParam = bodyParam.correspondingMethodParams[0];
diagnostics.pipe(
addEncodeInfo(
Expand Down Expand Up @@ -416,6 +432,7 @@ export function getSdkHttpParameter(
$(program).type.isAssignableTo(param.type, $(program).builtin.url, param.type),
serializedName: getPathParamName(program, param) ?? base.name,
correspondingMethodParams: [],
methodParameterSegments: [],
optional: param.optional,
});
}
Expand All @@ -425,6 +442,7 @@ export function getSdkHttpParameter(
kind: "cookie",
serializedName: getCookieParamOptions(program, param)?.name ?? base.name,
correspondingMethodParams: [],
methodParameterSegments: [],
optional: param.optional,
});
}
Expand All @@ -437,6 +455,7 @@ export function getSdkHttpParameter(
defaultContentType: "application/json",
optional: param.optional,
correspondingMethodParams: [],
methodParameterSegments: [],
serializationOptions: {},
});
}
Expand All @@ -445,6 +464,7 @@ export function getSdkHttpParameter(
optional: param.optional,
collectionFormat: diagnostics.pipe(getCollectionFormat(context, param)),
correspondingMethodParams: [],
methodParameterSegments: [],
};
if (isQueryParam(context.program, param) || location === "query") {
return diagnostics.wrap({
Expand Down Expand Up @@ -585,12 +605,22 @@ function getSdkHttpResponseAndExceptions(
return diagnostics.wrap({ responses, exceptions });
}

export function getCorrespondingMethodParams(
/**
* Get method parameter segments for a service parameter.
* This builds the complete path from method parameters to the HTTP parameter.
* For body parameters with spread, multiple paths may be returned.
* @param context
* @param operation
* @param methodParameters
* @param serviceParam
* @returns Array of path segments, where each inner array represents a complete path
*/
export function getMethodParameterSegments(
context: TCGCContext,
operation: Operation,
methodParameters: SdkMethodParameter[],
serviceParam: SdkHttpParameter,
): [(SdkMethodParameter | SdkModelPropertyType)[], readonly Diagnostic[]] {
): [(SdkMethodParameter | SdkModelPropertyType)[][], readonly Diagnostic[]] {
const diagnostics = createDiagnosticCollector();

if (serviceParam.onClient) {
Expand All @@ -601,7 +631,7 @@ export function getCorrespondingMethodParams(
serviceParam.__raw,
operation,
);
if (correspondingClientParam) return diagnostics.wrap([correspondingClientParam]);
if (correspondingClientParam) return diagnostics.wrap([[correspondingClientParam]]);
}

const clientParams = context.__clientParametersCache.get(
Expand All @@ -611,13 +641,13 @@ export function getCorrespondingMethodParams(
// 2. To see if the service parameter is api version parameter that has been elevated to client.
if (clientParams && serviceParam.isApiVersionParam && serviceParam.onClient) {
const existingApiVersion = clientParams.find((x) => isApiVersion(context, x.__raw!));
if (existingApiVersion) return diagnostics.wrap([existingApiVersion]);
if (existingApiVersion) return diagnostics.wrap([[existingApiVersion]]);
}

// 3. To see if the service parameter is subscription parameter that has been elevated to client (only for arm service).
if (clientParams && isSubscriptionId(context, serviceParam)) {
const subId = clientParams.find((x) => isSubscriptionId(context, x));
if (subId) return diagnostics.wrap([subId]);
if (subId) return diagnostics.wrap([[subId]]);
}
}

Expand All @@ -626,26 +656,27 @@ export function getCorrespondingMethodParams(
serviceParam.onClient = false;

// 4. To see if the service parameter is a method parameter or a property of a method parameter.
const directMapping = findMapping(context, methodParameters, serviceParam);
if (directMapping) {
return diagnostics.wrap([directMapping]);
const directMappingPath = findMappingWithPath(context, methodParameters, serviceParam);
if (directMappingPath) {
return diagnostics.wrap([directMappingPath]);
}

// 5. To see if all the property of the service parameter could be mapped to a method parameter or a property of a method parameter.
// This is the spread body case where multiple paths may exist.
if (serviceParam.kind === "body" && serviceParam.type.kind === "model") {
const retVal = [];
const paths: (SdkMethodParameter | SdkModelPropertyType)[][] = [];
let optionalSkip = 0;
for (const serviceParamProp of serviceParam.type.properties) {
const propertyMapping = findMapping(context, methodParameters, serviceParamProp);
if (propertyMapping) {
retVal.push(propertyMapping);
const propertyMappingPath = findMappingWithPath(context, methodParameters, serviceParamProp);
if (propertyMappingPath) {
paths.push(propertyMappingPath);
} else if (serviceParamProp.optional) {
// If the property is optional, we can skip the mapping.
optionalSkip++;
}
}
if (retVal.length + optionalSkip === serviceParam.type.properties.length) {
return diagnostics.wrap(retVal);
if (paths.length + optionalSkip === serviceParam.type.properties.length) {
return diagnostics.wrap(paths);
}
}

Expand All @@ -667,54 +698,65 @@ export function getCorrespondingMethodParams(
}

/**
* Try to find the mapping of a service paramete or a property of a service parameter to a method parameter or a property of a method parameter.
* Build path segments from method parameters to service parameter. The map could be a service parameter or a property of a service parameter to a method parameter or a property of a method parameter.
* This function finds the complete path from method parameter to the HTTP parameter.
* @param context
* @param methodParameters
* @param serviceParam
* @returns
* @returns An array of path segments, where each segment is a path from method parameter to the service parameter
*/
function findMapping(
function findMappingWithPath(
context: TCGCContext,
methodParameters: SdkMethodParameter[],
serviceParam: SdkHttpParameter | SdkModelPropertyType,
): SdkMethodParameter | SdkModelPropertyType | undefined {
const queue: (SdkMethodParameter | SdkModelPropertyType)[] = [...methodParameters];
): (SdkMethodParameter | SdkModelPropertyType)[] | undefined {
// Use a queue of tuples: [current parameter, path to reach it]
const queue: [
SdkMethodParameter | SdkModelPropertyType,
(SdkMethodParameter | SdkModelPropertyType)[],
][] = methodParameters.map((p) => [p, [p]]);
const visited: Set<SdkModelType> = new Set();

while (queue.length > 0) {
const methodParam = queue.shift()!;
const [methodParam, path] = queue.shift()!;

// HTTP operation parameter/body parameter/property of body parameter could either from an operation parameter directly or from a property of an operation parameter.
if (
methodParam.__raw &&
serviceParam.__raw &&
compareModelProperties(context, methodParam.__raw, serviceParam.__raw)
) {
return methodParam;
return path;
}

// Two following two hard coded mapping is for the case that TCGC help to add content type and accept header when not exists.
if (
serviceParam.kind === "header" &&
serviceParam.serializedName === "Content-Type" &&
methodParam.name === "contentType"
) {
return methodParam;
return path;
}
if (
serviceParam.kind === "header" &&
serviceParam.serializedName === "Accept" &&
methodParam.name === "accept"
) {
return methodParam;
return path;
}

// If the service parameter is a body parameter, try to see if we could find a method parameter with same type of the body parameter.
if (serviceParam.kind === "body" && serviceParam.type === methodParam.type) {
return methodParam;
return path;
}
// BFS to find the mapping.

// BFS to explore nested properties
if (methodParam.type.kind === "model" && !visited.has(methodParam.type)) {
visited.add(methodParam.type);
let current: SdkModelType | undefined = methodParam.type;
while (current) {
for (const prop of methodParam.type.properties) {
queue.push(prop);
for (const prop of current.properties) {
queue.push([prop, [...path, prop]]);
}
current = current.baseModel;
}
Expand Down
55 changes: 50 additions & 5 deletions packages/typespec-client-generator-core/src/interfaces.ts
Original file line number Diff line number Diff line change
Expand Up @@ -719,8 +719,17 @@ export interface SdkHeaderParameter extends SdkModelPropertyTypeBase {
collectionFormat?: CollectionFormat;
/** Name for the parameter in the payload */
serializedName: string;
/** Corresponding method level parameter or model property for current parameter. */
/**
* @deprecated This property is deprecated. Use `methodParameterSegments` instead.
* Corresponding method level parameter or model property for current parameter.
*/
correspondingMethodParams: (SdkMethodParameter | SdkModelPropertyType)[];
/**
* Segments to indicate the complete path from method parameters to this HTTP parameter.
* Each inner array represents a complete path from method parameter to the final HTTP parameter.
* For body parameters with spread, there can be multiple paths.
*/
methodParameterSegments: (SdkMethodParameter | SdkModelPropertyType)[][];
}

/**
Expand All @@ -731,8 +740,17 @@ export interface SdkQueryParameter extends SdkModelPropertyTypeBase {
collectionFormat?: CollectionFormat;
/** Name for the parameter in the payload */
serializedName: string;
/** Corresponding method level parameter or model property for current parameter. */
/**
* @deprecated This property is deprecated. Use `methodParameterSegments` instead.
* Corresponding method level parameter or model property for current parameter.
*/
correspondingMethodParams: (SdkMethodParameter | SdkModelPropertyType)[];
/**
* Segments to indicate the complete path from method parameters to this HTTP parameter.
* Each inner array represents a complete path from method parameter to the final HTTP parameter.
* For body parameters with spread, there can be multiple paths.
*/
methodParameterSegments: (SdkMethodParameter | SdkModelPropertyType)[][];
explode: boolean;
}

Expand All @@ -746,8 +764,17 @@ export interface SdkPathParameter extends SdkModelPropertyTypeBase {
allowReserved: boolean;
/** Name for the parameter in the payload */
serializedName: string;
/** Corresponding method level parameter or model property for current parameter. */
/**
* @deprecated This property is deprecated. Use `methodParameterSegments` instead.
* Corresponding method level parameter or model property for current parameter.
*/
correspondingMethodParams: (SdkMethodParameter | SdkModelPropertyType)[];
/**
* Segments to indicate the complete path from method parameters to this HTTP parameter.
* Each inner array represents a complete path from method parameter to the final HTTP parameter.
* For body parameters with spread, there can be multiple paths.
*/
methodParameterSegments: (SdkMethodParameter | SdkModelPropertyType)[][];
}

/**
Expand All @@ -757,8 +784,17 @@ export interface SdkCookieParameter extends SdkModelPropertyTypeBase {
kind: "cookie";
/** Name for the parameter in the payload */
serializedName: string;
/** Corresponding method level parameter or model property for current parameter. */
/**
* @deprecated This property is deprecated. Use `methodParameterSegments` instead.
* Corresponding method level parameter or model property for current parameter.
*/
correspondingMethodParams: (SdkMethodParameter | SdkModelPropertyType)[];
/**
* Segments to indicate the complete path from method parameters to this HTTP parameter.
* Each inner array represents a complete path from method parameter to the final HTTP parameter.
* For body parameters with spread, there can be multiple paths.
*/
methodParameterSegments: (SdkMethodParameter | SdkModelPropertyType)[][];
}

/**
Expand All @@ -770,8 +806,17 @@ export interface SdkBodyParameter extends SdkModelPropertyTypeBase {
serializedName: string;
contentTypes: string[];
defaultContentType: string;
/** Corresponding method level parameter or model property for current parameter. */
/**
* @deprecated This property is deprecated. Use `methodParameterSegments` instead.
* Corresponding method level parameter or model property for current parameter.
*/
correspondingMethodParams: (SdkMethodParameter | SdkModelPropertyType)[];
/**
* Segments to indicate the complete path from method parameters to this HTTP parameter.
* Each inner array represents a complete path from method parameter to the final HTTP parameter.
* For body parameters with spread, there can be multiple paths.
*/
methodParameterSegments: (SdkMethodParameter | SdkModelPropertyType)[][];
}

export type SdkHttpParameter =
Expand Down
Loading
Loading