diff --git a/.chronus/changes/continuation_token-2025-1-12-17-57-14.md b/.chronus/changes/continuation_token-2025-1-12-17-57-14.md new file mode 100644 index 0000000000..cf494827c0 --- /dev/null +++ b/.chronus/changes/continuation_token-2025-1-12-17-57-14.md @@ -0,0 +1,7 @@ +--- +changeKind: feature +packages: + - "@azure-tools/typespec-client-generator-core" +--- + +Add `pagingMetadata.continuationTokenParameterSegments` and `pagingMetadata.continuationTokenResponseSegments` to `SdkPagingServiceMetadata` to indicate the mapping of continuation token parameter and response. \ No newline at end of file diff --git a/.chronus/changes/continuation_token-2025-1-12-17-58-11.md b/.chronus/changes/continuation_token-2025-1-12-17-58-11.md new file mode 100644 index 0000000000..a70a2a9319 --- /dev/null +++ b/.chronus/changes/continuation_token-2025-1-12-17-58-11.md @@ -0,0 +1,7 @@ +--- +changeKind: deprecation +packages: + - "@azure-tools/typespec-client-generator-core" +--- + +Deprecate `__raw_paged_metadata`, `nextLinkPath` and `nextLinkOperation` in `SdkPagingServiceMethodOptions`. Use `pagingMetadata.__raw`, `pagingMetadata.nextLinkSegments` and `pagingMetadata.nextLinkOperation` instead. \ No newline at end of file diff --git a/.chronus/changes/continuation_token-2025-1-14-16-15-29.md b/.chronus/changes/continuation_token-2025-1-14-16-15-29.md new file mode 100644 index 0000000000..3c3d33d7f7 --- /dev/null +++ b/.chronus/changes/continuation_token-2025-1-14-16-15-29.md @@ -0,0 +1,7 @@ +--- +changeKind: feature +packages: + - "@azure-tools/typespec-client-generator-core" +--- + +Make `SdkServiceResponseHeader` to be part of `SdkModelPropertyType`. Then it could contain the client related info. \ No newline at end of file diff --git a/.chronus/changes/continuation_token-2025-1-14-16-17-22.md b/.chronus/changes/continuation_token-2025-1-14-16-17-22.md new file mode 100644 index 0000000000..6de7262633 --- /dev/null +++ b/.chronus/changes/continuation_token-2025-1-14-16-17-22.md @@ -0,0 +1,7 @@ +--- +changeKind: deprecation +packages: + - "@azure-tools/typespec-client-generator-core" +--- + +Deprecate `resultPath` in `SdkMethodResponse`. Use `resultSegments` instead. \ No newline at end of file diff --git a/.chronus/changes/continuation_token-2025-1-18-16-47-36.md b/.chronus/changes/continuation_token-2025-1-18-16-47-36.md new file mode 100644 index 0000000000..2ac3360fe1 --- /dev/null +++ b/.chronus/changes/continuation_token-2025-1-18-16-47-36.md @@ -0,0 +1,7 @@ +--- +changeKind: feature +packages: + - "@azure-tools/typespec-client-generator-core" +--- + +Add `SdkPagingServiceMetadata` type to store all paging related info. \ No newline at end of file diff --git a/packages/typespec-client-generator-core/src/context.ts b/packages/typespec-client-generator-core/src/context.ts index ce54f7b323..cf5b15e22d 100644 --- a/packages/typespec-client-generator-core/src/context.ts +++ b/packages/typespec-client-generator-core/src/context.ts @@ -17,6 +17,8 @@ import { SdkEmitterOptions, SdkEnumType, SdkHttpOperation, + SdkHttpParameter, + SdkMethodParameter, SdkModelPropertyType, SdkModelType, SdkNullableType, @@ -35,16 +37,21 @@ export function createTCGCContext(program: Program, emitterName?: string): TCGCC parseEmitterName(program, emitterName ?? program.emitters[0]?.metadata?.name), ), diagnostics: diagnostics.diagnostics, - originalProgram: program, + __originalProgram: program, __clientToParameters: new Map(), __tspTypeToApiVersions: new Map(), __clientToApiVersionClientDefaultValue: new Map(), previewStringRegex: /-preview$/, disableUsageAccessPropagationToBase: false, __pagedResultSet: new Set(), - referencedTypeMap: new Map(), - httpOperationCache: new Map(), - referencedPropertyMap: new Map(), + __referencedTypeCache: new Map< + Type, + SdkModelType | SdkEnumType | SdkUnionType | SdkNullableType + >(), + __httpOperationCache: new Map(), + __modelPropertyCache: new Map(), + __methodParameterCache: new Map(), + __httpParameterCache: new Map(), }; } diff --git a/packages/typespec-client-generator-core/src/decorators.ts b/packages/typespec-client-generator-core/src/decorators.ts index 4a71ca8f53..99483c036e 100644 --- a/packages/typespec-client-generator-core/src/decorators.ts +++ b/packages/typespec-client-generator-core/src/decorators.ts @@ -316,7 +316,7 @@ function serviceVersioningProjection(context: TCGCContext, client: SdkClient) { // TODO: THIS NEED TO BE MIGRATED BY MARCH 2024 release. // eslint-disable-next-line @typescript-eslint/no-deprecated projectedProgram = context.program = projectProgram( - context.originalProgram, + context.__originalProgram, projectedVersion.projections, ); } diff --git a/packages/typespec-client-generator-core/src/http.ts b/packages/typespec-client-generator-core/src/http.ts index 904b29f2d3..a1935862d9 100644 --- a/packages/typespec-client-generator-core/src/http.ts +++ b/packages/typespec-client-generator-core/src/http.ts @@ -49,6 +49,7 @@ import { TCGCContext, } from "./interfaces.js"; import { + findRootSourceProperty, getAvailableApiVersions, getHttpBodySpreadModel, getHttpOperationResponseHeaders, @@ -337,84 +338,88 @@ export function getSdkHttpParameter( location?: "path" | "query" | "header" | "body" | "cookie", ): [SdkHttpParameter, readonly Diagnostic[]] { const diagnostics = createDiagnosticCollector(); - const base = diagnostics.pipe(getSdkModelPropertyTypeBase(context, param, operation)); - const program = context.program; - const correspondingMethodParams: SdkParameter[] = []; // we set it later in the operation - if (isPathParam(context.program, param) || location === "path") { - // we don't url encode if the type can be assigned to url - const urlEncode = !ignoreDiagnostics( - program.checker.isTypeAssignableTo( - // TODO: THIS NEED TO BE MIGRATED BY MARCH 2024 release. - // eslint-disable-next-line @typescript-eslint/no-deprecated - param.type.projectionBase ?? param.type, - program.checker.getStdType("url"), - param.type, - ), - ); - return diagnostics.wrap({ - ...base, - kind: "path", - urlEncode, - explode: (httpParam as HttpOperationPathParameter)?.explode ?? false, - style: (httpParam as HttpOperationPathParameter)?.style ?? "simple", - allowReserved: (httpParam as HttpOperationPathParameter)?.allowReserved ?? false, - serializedName: getPathParamName(program, param) ?? base.name, - correspondingMethodParams, - optional: false, - }); - } - if (isCookieParam(context.program, param) || location === "cookie") { - return diagnostics.wrap({ - ...base, - kind: "cookie", - serializedName: getCookieParamOptions(program, param)?.name ?? base.name, - correspondingMethodParams, - optional: param.optional, - }); - } - if (isBody(context.program, param) || location === "body") { - return diagnostics.wrap({ - ...base, - kind: "body", - serializedName: param.name === "" ? "body" : getWireName(context, param), - contentTypes: ["application/json"], // will update when we get to the operation level - defaultContentType: "application/json", // will update when we get to the operation level - optional: param.optional, - correspondingMethodParams, - }); - } - const headerQueryBase = { - ...base, - optional: param.optional, - collectionFormat: getCollectionFormat(context, param), - correspondingMethodParams, - }; - if (isQueryParam(context.program, param) || location === "query") { - return diagnostics.wrap({ - ...headerQueryBase, - kind: "query", - serializedName: getQueryParamName(program, param) ?? base.name, - explode: (httpParam as HttpOperationQueryParameter)?.explode, - }); - } - if (!(isHeader(context.program, param) || location === "header")) { - diagnostics.add( - createDiagnostic({ - code: "unexpected-http-param-type", - target: param, - format: { - paramName: param.name, - expectedType: "path, query, header, or body", - actualType: param.kind, - }, - }), - ); + let parameter = context.__httpParameterCache?.get(param); + + if (!parameter) { + const base = diagnostics.pipe(getSdkModelPropertyTypeBase(context, param, operation)); + const program = context.program; + const correspondingMethodParams: SdkParameter[] = []; // we set it later in the operation + if (isPathParam(context.program, param) || location === "path") { + // we don't url encode if the type can be assigned to url + const urlEncode = !ignoreDiagnostics( + program.checker.isTypeAssignableTo( + // TODO: THIS NEED TO BE MIGRATED BY MARCH 2024 release. + // eslint-disable-next-line @typescript-eslint/no-deprecated + param.type.projectionBase ?? param.type, + program.checker.getStdType("url"), + param.type, + ), + ); + parameter = { + ...base, + kind: "path", + urlEncode, + explode: (httpParam as HttpOperationPathParameter)?.explode ?? false, + style: (httpParam as HttpOperationPathParameter)?.style ?? "simple", + allowReserved: (httpParam as HttpOperationPathParameter)?.allowReserved ?? false, + serializedName: getPathParamName(program, param) ?? base.name, + correspondingMethodParams, + optional: false, + }; + } else if (isCookieParam(context.program, param) || location === "cookie") { + parameter = { + ...base, + kind: "cookie", + serializedName: getCookieParamOptions(program, param)?.name ?? base.name, + correspondingMethodParams, + optional: param.optional, + }; + } else if (isBody(context.program, param) || location === "body") { + parameter = { + ...base, + kind: "body", + serializedName: param.name === "" ? "body" : getWireName(context, param), + contentTypes: ["application/json"], // will update when we get to the operation level + defaultContentType: "application/json", // will update when we get to the operation level + optional: param.optional, + correspondingMethodParams, + }; + } else if (isQueryParam(context.program, param) || location === "query") { + parameter = { + ...base, + optional: param.optional, + collectionFormat: getCollectionFormat(context, param), + correspondingMethodParams, + kind: "query", + serializedName: getQueryParamName(program, param) ?? base.name, + explode: (httpParam as HttpOperationQueryParameter)?.explode, + }; + } else { + if (!(isHeader(context.program, param) || location === "header")) { + diagnostics.add( + createDiagnostic({ + code: "unexpected-http-param-type", + target: param, + format: { + paramName: param.name, + expectedType: "path, query, header, or body", + actualType: param.kind, + }, + }), + ); + } + parameter = { + ...base, + optional: param.optional, + collectionFormat: getCollectionFormat(context, param), + correspondingMethodParams, + kind: "header", + serializedName: getHeaderFieldName(program, param) ?? base.name, + }; + } + context.__httpParameterCache.set(param, parameter); } - return diagnostics.wrap({ - ...headerQueryBase, - kind: "header", - serializedName: getHeaderFieldName(program, param) ?? base.name, - }); + return diagnostics.wrap(parameter); } function getSdkHttpResponseAndExceptions( @@ -441,14 +446,13 @@ function getSdkHttpResponseAndExceptions( : innerResponse.body?.contentTypes[0]; for (const header of getHttpOperationResponseHeaders(innerResponse)) { if (isNeverOrVoidType(header.type)) continue; - const clientType = diagnostics.pipe(getClientTypeWithDiagnostics(context, header.type)); - addEncodeInfo(context, header, clientType, defaultContentType); headers.push({ + ...diagnostics.pipe( + getSdkModelPropertyTypeBase(context, header, httpOperation.operation), + ), __raw: header, - doc: getDoc(context.program, header), - summary: getSummary(context.program, header), + kind: "responseheader", serializedName: getHeaderFieldName(context.program, header), - type: clientType, }); } if (innerResponse.body && !isNeverOrVoidType(innerResponse.body.type)) { @@ -693,13 +697,6 @@ function filterOutUselessPathParameters( } } -function findRootSourceProperty(property: ModelProperty): ModelProperty { - while (property.sourceProperty) { - property = property.sourceProperty; - } - return property; -} - function getCollectionFormat( context: TCGCContext, type: ModelProperty, diff --git a/packages/typespec-client-generator-core/src/interfaces.ts b/packages/typespec-client-generator-core/src/interfaces.ts index d31a73ce62..7bc6c76a8c 100644 --- a/packages/typespec-client-generator-core/src/interfaces.ts +++ b/packages/typespec-client-generator-core/src/interfaces.ts @@ -14,6 +14,7 @@ import { ModelProperty, Namespace, Operation, + PagingOperation, Program, ProjectedProgram, Type, @@ -33,32 +34,37 @@ import { TspLiteralType } from "./internal-utils.js"; export interface TCGCContext { program: Program; + diagnostics: readonly Diagnostic[]; emitterName: string; + arm?: boolean; + generateProtocolMethods?: boolean; generateConvenienceMethods?: boolean; packageName?: string; flattenUnionAsEnum?: boolean; - arm?: boolean; - referencedTypeMap: Map; - referencedPropertyMap: Map; - generatedNames?: Map; - httpOperationCache: Map; + apiVersion?: string; + examplesDir?: string; + + decoratorsAllowList?: string[]; + previewStringRegex: RegExp; + disableUsageAccessPropagationToBase: boolean; + + __referencedTypeCache: Map; + __modelPropertyCache: Map; + __methodParameterCache: Map; + __httpParameterCache: Map; + __generatedNames?: Map; + __httpOperationCache: Map; __clientToParameters: Map; __tspTypeToApiVersions: Map; __clientToApiVersionClientDefaultValue: Map; - knownScalars?: Record; - diagnostics: readonly Diagnostic[]; + __knownScalars?: Record; __rawClients?: SdkClient[]; - apiVersion?: string; // TODO: THIS NEED TO BE MIGRATED BY MARCH 2024 release. // eslint-disable-next-line @typescript-eslint/no-deprecated __service_projection?: Map; __httpOperationExamples?: Map; - originalProgram: Program; - examplesDir?: string; - decoratorsAllowList?: string[]; - previewStringRegex: RegExp; - disableUsageAccessPropagationToBase: boolean; + __originalProgram: Program; __pagedResultSet: Set; } @@ -593,7 +599,8 @@ export type SdkModelPropertyType = | SdkPathParameter | SdkBodyParameter | SdkHeaderParameter - | SdkCookieParameter; + | SdkCookieParameter + | SdkServiceResponseHeader; export interface MultipartOptions { name: string; @@ -686,21 +693,21 @@ export interface SdkMethodParameter extends SdkModelPropertyTypeBase { kind: "method"; } -export interface SdkServiceResponseHeader { +export interface SdkServiceResponseHeader extends SdkModelPropertyTypeBase { __raw: ModelProperty; + kind: "responseheader"; serializedName: string; - type: SdkType; - doc?: string; - summary?: string; } export interface SdkMethodResponse { kind: "method"; type?: SdkType; - resultPath?: string; // if exists, tells you how to get from the service response to the method response. /** - * An array of properties to fetch {result} from the {response} model. Note that this property is available only in some LRO patterns. - * Temporarily this is not enabled for paging now. + * @deprecated Use `resultSegments` instead. + */ + resultPath?: string; + /** + * An array of properties to fetch {result} from the {response} model. Note that this property is only for LRO and paging pattens. */ resultSegments?: SdkModelPropertyType[]; } @@ -778,17 +785,42 @@ export interface SdkBasicServiceMethod { + /** + * @deprecated Use `pagingMetadata.__raw` instead. + */ __raw_paged_metadata?: PagedResultMetadata; + /** + * @deprecated Use `pagingMetadata.nextLinkSegments` instead. + */ nextLinkPath?: string; + /** + * @deprecated Use `pagingMetadata.nextLinkOperation` instead. + */ nextLinkOperation?: SdkServiceOperation; - continuationTokenParameter?: SdkMethodParameter; + pagingMetadata: SdkPagingServiceMetadata; +} + +/** + * Paging operation metadata. + */ +export interface SdkPagingServiceMetadata { + /** Paging metadata from TypeSpec core library. */ + __raw?: PagedResultMetadata | PagingOperation; + + /** Segments to indicate how to get next page link value from response. */ + nextLinkSegments?: SdkModelPropertyType[]; + /** Method used to get next page. If not defined, use the initial method. */ + nextLinkOperation?: SdkServiceMethod; + /** Segments to indicate how to set continuation token for next page request. */ + continuationTokenParameterSegments?: SdkModelPropertyType[]; + /** Segments to indicate how to get continuation token value from response. */ continuationTokenResponseSegments?: SdkModelPropertyType[]; } export interface SdkPagingServiceMethod extends SdkServiceMethodBase, - SdkPagingServiceMethodOptions { + SdkPagingServiceMethodOptions { kind: "paging"; } @@ -864,7 +896,7 @@ export interface SdkLroServiceMethod extends SdkServiceMethodBase, SdkLroServiceMethodOptions, - SdkPagingServiceMethodOptions { + SdkPagingServiceMethodOptions { kind: "lropaging"; } diff --git a/packages/typespec-client-generator-core/src/internal-utils.ts b/packages/typespec-client-generator-core/src/internal-utils.ts index 6155bb1797..560f8316b7 100644 --- a/packages/typespec-client-generator-core/src/internal-utils.ts +++ b/packages/typespec-client-generator-core/src/internal-utils.ts @@ -588,3 +588,10 @@ export function hasNoneVisibility(context: TCGCContext, type: ModelProperty): bo const visibility = getVisibilityForClass(context.program, type, lifecycle); return visibility.size === 0; } + +export function findRootSourceProperty(property: ModelProperty): ModelProperty { + while (property.sourceProperty) { + property = property.sourceProperty; + } + return property; +} diff --git a/packages/typespec-client-generator-core/src/package.ts b/packages/typespec-client-generator-core/src/package.ts index 208a5b4cfa..afd8240bc7 100644 --- a/packages/typespec-client-generator-core/src/package.ts +++ b/packages/typespec-client-generator-core/src/package.ts @@ -12,7 +12,7 @@ import { ModelProperty, Operation, } from "@typespec/compiler"; -import { getServers, HttpServer } from "@typespec/http"; +import { getServers, HttpServer, isHeader } from "@typespec/http"; import { resolveVersions } from "@typespec/versioning"; import { getAccess, @@ -64,6 +64,7 @@ import { import { createGeneratedName, filterApiVersionsWithDecorators, + findRootSourceProperty, getAllResponseBodiesAndNonBodyExists, getAvailableApiVersions, getClientNamespaceStringHelper, @@ -137,12 +138,12 @@ function getSdkPagingServiceMethod, readonly Diagnostic[]] { const diagnostics = createDiagnosticCollector(); - const basic = diagnostics.pipe( + const baseServiceMethod = diagnostics.pipe( getSdkBasicServiceMethod(context, operation, client), ); // nullable response type means the underlaying operation has multiple responses and only one of them is not empty, which is what we want - let responseType = basic.response.type; + let responseType = baseServiceMethod.response.type; if (responseType?.kind === "nullable") { responseType = responseType.type; } @@ -151,7 +152,11 @@ function getSdkPagingServiceMethod p === pagingOperation.output.pageItems.property, ); - const nextLinkPath = pagingOperation.output.nextLink - ? getPropertyPathFromModel( - context, - responseType?.__raw, - (p) => p === pagingOperation.output.nextLink!.property, - ) - : undefined; + baseServiceMethod.response.resultSegments = getPropertySegmentsFromModelOrParameters( + responseType, + (p) => p.__raw === pagingOperation.output.pageItems.property, + ); + + let nextLinkPath = undefined; + let nextLinkSegments = undefined; + if (pagingOperation.output.nextLink) { + nextLinkPath = getPropertyPathFromModel( + context, + responseType?.__raw, + (p) => p === pagingOperation.output.nextLink!.property, + ); + nextLinkSegments = getPropertySegmentsFromModelOrParameters( + responseType, + (p) => p.__raw === pagingOperation.output.nextLink?.property, + ); + } + + let continuationTokenParameterSegments = undefined; + let continuationTokenResponseSegments = undefined; + if (pagingOperation.input.continuationToken) { + continuationTokenParameterSegments = getPropertySegmentsFromModelOrParameters( + baseServiceMethod.parameters, + (p) => + p.__raw?.kind === "ModelProperty" && + findRootSourceProperty(p.__raw) === + findRootSourceProperty(pagingOperation.input.continuationToken!.property), + ); + } + if (pagingOperation.output.continuationToken) { + if (isHeader(context.program, pagingOperation.output.continuationToken.property)) { + continuationTokenResponseSegments = baseServiceMethod.operation.responses + .map((r) => r.headers) + .flat() + .filter((h) => h.__raw === pagingOperation.output.continuationToken!.property); + } else { + continuationTokenResponseSegments = getPropertySegmentsFromModelOrParameters( + responseType, + (p) => p.__raw === pagingOperation.output.continuationToken!.property, + ); + } + } context.__pagedResultSet.add(responseType); // tcgc will let all paging method return a list of items - basic.response.type = diagnostics.pipe( + baseServiceMethod.response.type = diagnostics.pipe( getClientTypeWithDiagnostics(context, pagingOperation?.output.pageItems.property.type), ); return diagnostics.wrap({ - ...basic, + ...baseServiceMethod, kind: "paging", nextLinkPath, + pagingMetadata: { + __raw: pagingOperation, + nextLinkSegments, + continuationTokenParameterSegments, + continuationTokenResponseSegments, + }, }); } // azure core paging const pagedMetadata = getPagedResult(context.program, operation)!; - if (responseType?.__raw?.kind !== "Model" || !pagedMetadata.itemsProperty) { + if ( + responseType?.__raw?.kind !== "Model" || + responseType.kind !== "model" || + !pagedMetadata.itemsProperty + ) { diagnostics.add( createDiagnostic({ code: "unexpected-pageable-operation-return-type", @@ -209,42 +262,71 @@ function getSdkPagingServiceMethod p.__raw === pagedMetadata.itemsProperty, + ); - return diagnostics.wrap({ - ...basic, - __raw_paged_metadata: pagedMetadata, - kind: "paging", - nextLinkPath: getPropertyPathFromSegment( + let nextLinkPath = undefined; + let nextLinkSegments = undefined; + if (pagedMetadata.nextLinkProperty) { + nextLinkPath = getPropertyPathFromSegment( context, pagedMetadata.modelType, pagedMetadata?.nextLinkSegments, - ), + ); + nextLinkSegments = getPropertySegmentsFromModelOrParameters( + responseType, + (p) => p.__raw === pagedMetadata.nextLinkProperty, + ); + } + + return diagnostics.wrap({ + ...baseServiceMethod, + __raw_paged_metadata: pagedMetadata, + kind: "paging", + nextLinkPath, nextLinkOperation: pagedMetadata?.nextLinkOperation ? diagnostics.pipe( getSdkServiceOperation( context, pagedMetadata.nextLinkOperation, - basic.parameters, + baseServiceMethod.parameters, ), ) : undefined, + pagingMetadata: { + __raw: pagedMetadata, + nextLinkSegments, + nextLinkOperation: pagedMetadata?.nextLinkOperation + ? diagnostics.pipe( + getSdkServiceMethod( + context, + pagedMetadata.nextLinkOperation, + client, + ), + ) + : undefined, + }, }); } @@ -255,6 +337,11 @@ export function getPropertyPathFromModel( ): string | undefined { const queue: { model: Model; path: ModelProperty[] }[] = []; + if (model.baseModel) { + const baseResult = getPropertyPathFromModel(context, model.baseModel, predicate); + if (baseResult) return baseResult; + } + for (const prop of model.properties.values()) { if (predicate(prop)) { return getLibraryName(context, prop); @@ -282,6 +369,43 @@ export function getPropertyPathFromModel( return undefined; } +export function getPropertySegmentsFromModelOrParameters( + source: SdkModelType | SdkMethodParameter[], + predicate: (property: SdkModelPropertyType) => boolean, +): SdkModelPropertyType[] | undefined { + const queue: { model: SdkModelType; path: SdkModelPropertyType[] }[] = []; + + if (!Array.isArray(source)) { + if (source.baseModel) { + const baseResult = getPropertySegmentsFromModelOrParameters(source.baseModel, predicate); + if (baseResult) return baseResult; + } + } + + for (const prop of Array.isArray(source) ? source : source.properties.values()) { + if (predicate(prop)) { + return [prop]; + } + if (prop.type.kind === "model") { + queue.push({ model: prop.type, path: [prop] }); + } + } + + while (queue.length > 0) { + const { model, path } = queue.shift()!; + for (const prop of model.properties.values()) { + if (predicate(prop)) { + return path.concat(prop); + } + if (prop.type.kind === "model") { + queue.push({ model: prop.type, path: path.concat(prop) }); + } + } + } + + return undefined; +} + function getPropertyPathFromSegment( context: TCGCContext, type: Model, @@ -313,19 +437,18 @@ function getSdkLroServiceMethod( ): [SdkLroServiceMethod, readonly Diagnostic[]] { const diagnostics = createDiagnosticCollector(); const metadata = getServiceMethodLroMetadata(context, operation)!; - const basicServiceMethod = diagnostics.pipe( + const baseServiceMethod = diagnostics.pipe( getSdkBasicServiceMethod(context, operation, client), ); - basicServiceMethod.response.type = metadata.finalResponse?.result; + baseServiceMethod.response.type = metadata.finalResponse?.result; // eslint-disable-next-line @typescript-eslint/no-deprecated - basicServiceMethod.response.resultPath = metadata.finalResponse?.resultPath; - - basicServiceMethod.response.resultSegments = metadata.finalResponse?.resultSegments; + baseServiceMethod.response.resultPath = metadata.finalResponse?.resultPath; + baseServiceMethod.response.resultSegments = metadata.finalResponse?.resultSegments; return diagnostics.wrap({ - ...basicServiceMethod, + ...baseServiceMethod, kind: "lro", __raw_lro_metadata: metadata.__raw, lroMetadata: metadata, @@ -333,7 +456,7 @@ function getSdkLroServiceMethod( getSdkServiceOperation( context, metadata.__raw.operation, - basicServiceMethod.parameters, + baseServiceMethod.parameters, ), ), }); @@ -689,10 +812,15 @@ function getSdkMethodParameter( operation: Operation, ): [SdkMethodParameter, readonly Diagnostic[]] { const diagnostics = createDiagnosticCollector(); - return diagnostics.wrap({ - ...diagnostics.pipe(getSdkModelPropertyType(context, type, operation)), - kind: "method", - }); + let parameter = context.__methodParameterCache.get(type); + if (!parameter) { + parameter = { + ...diagnostics.pipe(getSdkModelPropertyType(context, type, operation)), + kind: "method", + }; + context.__methodParameterCache.set(type, parameter); + } + return diagnostics.wrap(parameter as SdkMethodParameter); } function getSdkMethods( diff --git a/packages/typespec-client-generator-core/src/public-utils.ts b/packages/typespec-client-generator-core/src/public-utils.ts index 64b9d4f9dd..c7a37c5d9b 100644 --- a/packages/typespec-client-generator-core/src/public-utils.ts +++ b/packages/typespec-client-generator-core/src/public-utils.ts @@ -322,10 +322,10 @@ export function getGeneratedName( type: Model | Union | TspLiteralType, operation?: Operation, ): string { - if (!context.generatedNames) { - context.generatedNames = new Map(); + if (!context.__generatedNames) { + context.__generatedNames = new Map(); } - const generatedName = context.generatedNames.get(type); + const generatedName = context.__generatedNames.get(type); if (generatedName) return generatedName; const contextPath = operation @@ -645,14 +645,16 @@ function buildNameFromContextPaths( // 3. simplely handle duplication let duplicateCount = 1; const rawCreateName = createName; - const generatedNames = [...(context.generatedNames?.values() ?? [])]; + const generatedNames = [...(context.__generatedNames?.values() ?? [])]; while (generatedNames.includes(createName)) { createName = `${rawCreateName}${duplicateCount++}`; } - if (context.generatedNames) { - context.generatedNames.set(type, createName); + if (context.__generatedNames) { + context.__generatedNames.set(type, createName); } else { - context.generatedNames = new Map([[type, createName]]); + context.__generatedNames = new Map([ + [type, createName], + ]); } return createName; } @@ -661,11 +663,11 @@ export function getHttpOperationWithCache( context: TCGCContext, operation: Operation, ): HttpOperation { - if (context.httpOperationCache?.has(operation)) { - return context.httpOperationCache.get(operation)!; + if (context.__httpOperationCache?.has(operation)) { + return context.__httpOperationCache.get(operation)!; } const httpOperation = ignoreDiagnostics(getHttpOperation(context.program, operation)); - context.httpOperationCache?.set(operation, httpOperation); + context.__httpOperationCache?.set(operation, httpOperation); return httpOperation; } diff --git a/packages/typespec-client-generator-core/src/types.ts b/packages/typespec-client-generator-core/src/types.ts index 799199ed67..18dcb92f4e 100644 --- a/packages/typespec-client-generator-core/src/types.ts +++ b/packages/typespec-client-generator-core/src/types.ts @@ -41,6 +41,7 @@ import { getAuthentication, getHttpPart, getServers, + isBody, isHeader, isOrExtendsHttpFile, isStatusCode, @@ -118,7 +119,6 @@ import { import { getVersions } from "@typespec/versioning"; import { getNs, isAttribute, isUnwrapped } from "@typespec/xml"; -import { getSdkHttpParameter, isSdkHttpParameter } from "./http.js"; import { isMediaTypeJson, isMediaTypeXml } from "./media-types.js"; export function getTypeSpecBuiltInType( @@ -501,7 +501,7 @@ export function getSdkUnionWithDiagnostics( type: Union, operation?: Operation, ): [SdkType, readonly Diagnostic[]] { - let retval: SdkType | undefined = context.referencedTypeMap?.get(type); + let retval: SdkType | undefined = context.__referencedTypeCache?.get(type); const diagnostics = createDiagnosticCollector(); if (!retval) { @@ -809,7 +809,7 @@ export function getSdkModelWithDiagnostics( operation?: Operation, ): [SdkModelType, readonly Diagnostic[]] { const diagnostics = createDiagnosticCollector(); - let sdkType = context.referencedTypeMap?.get(type) as SdkModelType | undefined; + let sdkType = context.__referencedTypeCache?.get(type) as SdkModelType | undefined; if (!sdkType) { const name = getLibraryName(context, type) || getGeneratedName(context, type, operation); @@ -848,7 +848,7 @@ export function getSdkModelWithDiagnostics( // propreties should be generated first since base model'sdiscriminator handling is depend on derived model's properties diagnostics.pipe(addPropertiesToModelType(context, type, sdkType, operation)); if (type.baseModel) { - sdkType.baseModel = context.referencedTypeMap?.get(type.baseModel) as + sdkType.baseModel = context.__referencedTypeCache?.get(type.baseModel) as | SdkModelType | undefined; if (sdkType.baseModel === undefined) { @@ -945,7 +945,7 @@ function getSdkEnumWithDiagnostics( operation?: Operation, ): [SdkEnumType, readonly Diagnostic[]] { const diagnostics = createDiagnosticCollector(); - let sdkType = context.referencedTypeMap?.get(type) as SdkEnumType | undefined; + let sdkType = context.__referencedTypeCache?.get(type) as SdkEnumType | undefined; if (!sdkType) { const namespace = getClientNamespace(context, type); sdkType = { @@ -1050,8 +1050,8 @@ export function getClientTypeWithDiagnostics( type: Type, operation?: Operation, ): [SdkType, readonly Diagnostic[]] { - if (!context.knownScalars) { - context.knownScalars = getKnownScalars(); + if (!context.__knownScalars) { + context.__knownScalars = getKnownScalars(); } const diagnostics = createDiagnosticCollector(); let retval: SdkType | undefined = undefined; @@ -1270,7 +1270,7 @@ function isFilePart(context: TCGCContext, type: SdkType): boolean { return true; } // HttpPart<{@body body: bytes}> or HttpPart<{@body body: File}> - const body = type.properties.find((x) => x.kind === "body"); + const body = type.properties.find((x) => x.__raw && isBody(context.program, x.__raw)); if (body) { return isFilePart(context, body.type); } @@ -1394,7 +1394,7 @@ export function getSdkModelPropertyType( ): [SdkModelPropertyType, readonly Diagnostic[]] { const diagnostics = createDiagnosticCollector(); - let property = context.referencedPropertyMap?.get(type); + let property = context.__modelPropertyCache?.get(type); if (!property) { const clientParams = operation @@ -1406,7 +1406,6 @@ export function getSdkModelPropertyType( if (correspondingClientParams) return diagnostics.wrap(correspondingClientParams); const base = diagnostics.pipe(getSdkModelPropertyTypeBase(context, type, operation)); - if (isSdkHttpParameter(context, type)) return getSdkHttpParameter(context, type, operation!); property = { ...base, kind: "property", @@ -1470,7 +1469,7 @@ function updateReferencedPropertyMap( if (sdkType.kind !== "property") { return; } - context.referencedPropertyMap.set(type, sdkType); + context.__modelPropertyCache.set(type, sdkType); } function updateReferencedTypeMap(context: TCGCContext, type: Type, sdkType: SdkType) { @@ -1482,7 +1481,7 @@ function updateReferencedTypeMap(context: TCGCContext, type: Type, sdkType: SdkT ) { return; } - context.referencedTypeMap?.set(type, sdkType); + context.__referencedTypeCache?.set(type, sdkType); } interface PropagationOptions { @@ -1809,13 +1808,13 @@ function updateTypesFromOperation( function updateAccessOverride(context: TCGCContext): [void, readonly Diagnostic[]] { const diagnostics = createDiagnosticCollector(); // set access for all orphan model without override - for (const sdkType of context.referencedTypeMap?.values() ?? []) { + for (const sdkType of context.__referencedTypeCache?.values() ?? []) { const accessOverride = getAccessOverride(context, sdkType.__raw as any); if (!sdkType.__accessSet && accessOverride === undefined) { diagnostics.pipe(updateUsageOrAccess(context, "public", sdkType)); } } - for (const sdkType of context.referencedTypeMap?.values() ?? []) { + for (const sdkType of context.__referencedTypeCache?.values() ?? []) { const accessOverride = getAccessOverride(context, sdkType.__raw as any); if (accessOverride) { diagnostics.pipe(updateUsageOrAccess(context, accessOverride, sdkType, { isOverride: true })); @@ -1826,7 +1825,7 @@ function updateAccessOverride(context: TCGCContext): [void, readonly Diagnostic[ function updateUsageOverride(context: TCGCContext): [void, readonly Diagnostic[]] { const diagnostics = createDiagnosticCollector(); - for (const sdkType of context.referencedTypeMap?.values() ?? []) { + for (const sdkType of context.__referencedTypeCache?.values() ?? []) { const usageOverride = getUsageOverride(context, sdkType.__raw as any); if (usageOverride) { diagnostics.pipe(updateUsageOrAccess(context, usageOverride, sdkType, { isOverride: true })); @@ -1836,7 +1835,7 @@ function updateUsageOverride(context: TCGCContext): [void, readonly Diagnostic[] } function updateSpreadModelUsageAndAccess(context: TCGCContext): void { - for (const [_, sdkType] of context.referencedTypeMap?.entries() ?? []) { + for (const [_, sdkType] of context.__referencedTypeCache?.entries() ?? []) { if ( sdkType.kind === "model" && (sdkType.usage & UsageFlags.Spread) > 0 && @@ -1870,7 +1869,7 @@ function filterOutTypes( filter: number, ): (SdkModelType | SdkEnumType | SdkUnionType | SdkNullableType)[] { const result = new Array(); - for (const sdkType of context.referencedTypeMap?.values() ?? []) { + for (const sdkType of context.__referencedTypeCache?.values() ?? []) { // filter models with unexpected usage if ((sdkType.usage & filter) === 0) { continue; diff --git a/packages/typespec-client-generator-core/test/package.test.ts b/packages/typespec-client-generator-core/test/package.test.ts index bfbc873eaa..2afee4c3cf 100644 --- a/packages/typespec-client-generator-core/test/package.test.ts +++ b/packages/typespec-client-generator-core/test/package.test.ts @@ -813,6 +813,11 @@ describe("typespec-client-generator-core: package", () => { strictEqual(methodResponse.kind, "method"); strictEqual(methodResponse.type, widgetModel); strictEqual(createOrUpdate.response.resultPath, "result"); + strictEqual(createOrUpdate.response.resultSegments?.length, 1); + strictEqual( + createOrUpdate.response.resultSegments[0], + createOrUpdate.lroMetadata.finalResponse?.envelopeResult.properties[3], + ); }); it("lro delete", async () => { const runnerWithCore = await createSdkTestRunner({ @@ -948,7 +953,7 @@ describe("typespec-client-generator-core: package", () => { (x) => x.name === "clientRequestId", ); ok(clientRequestIdProperty); - strictEqual(clientRequestIdProperty.kind, "header"); + strictEqual(clientRequestIdProperty.kind, "property"); }); it("azure widget getWidgetAnalytics", async () => { diff --git a/packages/typespec-client-generator-core/test/packages/paged-operation.test.ts b/packages/typespec-client-generator-core/test/packages/paged-operation.test.ts index 6cabefc163..c2caddbb25 100644 --- a/packages/typespec-client-generator-core/test/packages/paged-operation.test.ts +++ b/packages/typespec-client-generator-core/test/packages/paged-operation.test.ts @@ -1,8 +1,12 @@ import { AzureCoreTestLibrary } from "@azure-tools/typespec-azure-core/testing"; import { Model, ModelProperty } from "@typespec/compiler"; -import { strictEqual } from "assert"; +import { deepStrictEqual, strictEqual } from "assert"; import { beforeEach, describe, it } from "vitest"; -import { getPropertyPathFromModel } from "../../src/package.js"; +import { SdkMethodParameter } from "../../src/interfaces.js"; +import { + getPropertyPathFromModel, + getPropertySegmentsFromModelOrParameters, +} from "../../src/package.js"; import { SdkTestRunner, createSdkTestRunner } from "../test-host.js"; import { getServiceMethodOfClient } from "./utils.js"; @@ -38,10 +42,14 @@ describe("typespec-client-generator-core: paged operation", () => { strictEqual(method.name, "test"); strictEqual(method.kind, "paging"); strictEqual(method.nextLinkPath, "nextLink"); + strictEqual(method.pagingMetadata.nextLinkSegments?.length, 1); + strictEqual(method.pagingMetadata.nextLinkSegments[0], sdkPackage.models[0].properties[1]); const response = method.response; strictEqual(response.kind, "method"); strictEqual(response.resultPath, "values"); + strictEqual(response.resultSegments?.length, 1); + strictEqual(response.resultSegments[0], sdkPackage.models[0].properties[0]); }); it("normal paged result", async () => { @@ -63,10 +71,14 @@ describe("typespec-client-generator-core: paged operation", () => { strictEqual(method.name, "test"); strictEqual(method.kind, "paging"); strictEqual(method.nextLinkPath, "next"); + strictEqual(method.pagingMetadata.nextLinkSegments?.length, 1); + strictEqual(method.pagingMetadata.nextLinkSegments[0], sdkPackage.models[0].properties[1]); const response = method.response; strictEqual(response.kind, "method"); strictEqual(response.resultPath, "tests"); + strictEqual(response.resultSegments?.length, 1); + strictEqual(response.resultSegments[0], sdkPackage.models[0].properties[0]); }); it("nullable paged result", async () => { @@ -88,10 +100,14 @@ describe("typespec-client-generator-core: paged operation", () => { strictEqual(method.name, "test"); strictEqual(method.kind, "paging"); strictEqual(method.nextLinkPath, "next"); + strictEqual(method.pagingMetadata.nextLinkSegments?.length, 1); + strictEqual(method.pagingMetadata.nextLinkSegments[0], sdkPackage.models[0].properties[1]); const response = method.response; strictEqual(response.kind, "method"); strictEqual(response.resultPath, "tests"); + strictEqual(response.resultSegments?.length, 1); + strictEqual(response.resultSegments[0], sdkPackage.models[0].properties[0]); }); it("normal paged result with encoded name", async () => { @@ -115,10 +131,14 @@ describe("typespec-client-generator-core: paged operation", () => { strictEqual(method.name, "test"); strictEqual(method.kind, "paging"); strictEqual(method.nextLinkPath, "nextLink"); + strictEqual(method.pagingMetadata.nextLinkSegments?.length, 1); + strictEqual(method.pagingMetadata.nextLinkSegments[0], sdkPackage.models[0].properties[1]); const response = method.response; strictEqual(response.kind, "method"); strictEqual(response.resultPath, "values"); + strictEqual(response.resultSegments?.length, 1); + strictEqual(response.resultSegments[0], sdkPackage.models[0].properties[0]); }); // skip for current paging implementation does not support nested paging value @@ -145,10 +165,21 @@ describe("typespec-client-generator-core: paged operation", () => { strictEqual(method.name, "test"); strictEqual(method.kind, "paging"); strictEqual(method.nextLinkPath, "pagination.nextLink"); + strictEqual(method.pagingMetadata.nextLinkSegments?.length, 2); + strictEqual(method.pagingMetadata.nextLinkSegments[0], sdkPackage.models[0].properties[1]); + strictEqual(sdkPackage.models[0].properties[1].type.kind, "model"); + strictEqual( + method.pagingMetadata.nextLinkSegments[1], + sdkPackage.models[0].properties[1].type.properties[0], + ); const response = method.response; strictEqual(response.kind, "method"); strictEqual(response.resultPath, "results.values"); + strictEqual(response.resultSegments?.length, 2); + strictEqual(response.resultSegments[0], sdkPackage.models[0].properties[0]); + strictEqual(sdkPackage.models[0].properties[0].type.kind, "model"); + strictEqual(response.resultSegments[1], sdkPackage.models[0].properties[0].type.properties[0]); }); it("getPropertyPathFromModel test for nested case", async () => { @@ -203,9 +234,329 @@ describe("typespec-client-generator-core: paged operation", () => { strictEqual(method.name, "test"); strictEqual(method.kind, "paging"); strictEqual(method.nextLinkPath, "nextLink"); + strictEqual(method.pagingMetadata.nextLinkSegments?.length, 1); + strictEqual(method.pagingMetadata.nextLinkSegments[0], sdkPackage.models[1].properties[1]); const response = method.response; strictEqual(response.kind, "method"); strictEqual(response.resultPath, "values"); + strictEqual(response.resultSegments?.length, 1); + strictEqual(response.resultSegments[0], sdkPackage.models[1].properties[0]); + }); + + describe("common paging with continuation token", () => { + it("continuation token in response body and query parameter", async () => { + await runner.compileWithBuiltInService(` + @list + op test(@continuationToken @query token?: string): ListTestResult; + model ListTestResult { + @pageItems + items: Test[]; + @continuationToken + nextToken?: string; + } + model Test { + prop: string; + } + `); + const sdkPackage = runner.context.sdkPackage; + const method = getServiceMethodOfClient(sdkPackage); + strictEqual(method.name, "test"); + strictEqual(method.kind, "paging"); + strictEqual(method.pagingMetadata.continuationTokenParameterSegments?.length, 1); + strictEqual( + method.pagingMetadata.continuationTokenParameterSegments?.[0], + method.parameters[0], + ); + strictEqual( + method.operation.parameters[0].correspondingMethodParams[0], + method.parameters[0], + ); + strictEqual(method.pagingMetadata.continuationTokenResponseSegments?.length, 1); + strictEqual(method.operation.responses[0].type?.kind, "model"); + strictEqual( + method.pagingMetadata.continuationTokenResponseSegments?.[0], + method.operation.responses[0].type.properties[1], + ); + }); + + it("continuation token in response body and header parameter", async () => { + await runner.compileWithBuiltInService(` + @list + op test(@continuationToken @header token?: string): ListTestResult; + model ListTestResult { + @pageItems + items: Test[]; + @continuationToken + nextToken?: string; + } + model Test { + prop: string; + } + `); + const sdkPackage = runner.context.sdkPackage; + const method = getServiceMethodOfClient(sdkPackage); + strictEqual(method.name, "test"); + strictEqual(method.kind, "paging"); + strictEqual(method.pagingMetadata.continuationTokenParameterSegments?.length, 1); + strictEqual( + method.pagingMetadata.continuationTokenParameterSegments?.[0], + method.parameters[0], + ); + strictEqual(method.pagingMetadata.continuationTokenResponseSegments?.length, 1); + strictEqual(method.operation.responses[0].type?.kind, "model"); + strictEqual( + method.pagingMetadata.continuationTokenResponseSegments?.[0], + method.operation.responses[0].type.properties[1], + ); + }); + + it("continuation token in response body and body parameter", async () => { + await runner.compileWithBuiltInService(` + @list + op test(@continuationToken token?: string): ListTestResult; + model ListTestResult { + @pageItems + items: Test[]; + @continuationToken + nextToken?: string; + } + model Test { + prop: string; + } + `); + const sdkPackage = runner.context.sdkPackage; + const method = getServiceMethodOfClient(sdkPackage); + strictEqual(method.name, "test"); + strictEqual(method.kind, "paging"); + strictEqual(method.pagingMetadata.continuationTokenParameterSegments?.length, 1); + strictEqual( + method.pagingMetadata.continuationTokenParameterSegments?.[0], + method.parameters[0], + ); + strictEqual(method.pagingMetadata.continuationTokenResponseSegments?.length, 1); + strictEqual(method.operation.responses[0].type?.kind, "model"); + strictEqual( + method.pagingMetadata.continuationTokenResponseSegments?.[0], + method.operation.responses[0].type.properties[1], + ); + }); + + it("continuation token in response header and query parameter", async () => { + await runner.compileWithBuiltInService(` + @list + op test(@continuationToken @query token?: string): ListTestResult; + model ListTestResult { + @pageItems + items: Test[]; + @continuationToken + @header + nextToken?: string; + } + model Test { + prop: string; + } + `); + const sdkPackage = runner.context.sdkPackage; + const method = getServiceMethodOfClient(sdkPackage); + strictEqual(method.name, "test"); + strictEqual(method.kind, "paging"); + strictEqual(method.pagingMetadata.continuationTokenParameterSegments?.length, 1); + strictEqual( + method.pagingMetadata.continuationTokenParameterSegments?.[0], + method.parameters[0], + ); + strictEqual(method.pagingMetadata.continuationTokenResponseSegments?.length, 1); + strictEqual(method.operation.responses[0].type?.kind, "model"); + strictEqual( + method.pagingMetadata.continuationTokenResponseSegments?.[0], + method.operation.responses[0].headers[0], + ); + }); + + it("continuation token in response header and header parameter", async () => { + await runner.compileWithBuiltInService(` + @list + op test(@continuationToken @header token?: string): ListTestResult; + model ListTestResult { + @pageItems + items: Test[]; + @continuationToken + @header + nextToken?: string; + } + model Test { + prop: string; + } + `); + const sdkPackage = runner.context.sdkPackage; + const method = getServiceMethodOfClient(sdkPackage); + strictEqual(method.name, "test"); + strictEqual(method.kind, "paging"); + strictEqual(method.pagingMetadata.continuationTokenParameterSegments?.length, 1); + strictEqual( + method.pagingMetadata.continuationTokenParameterSegments?.[0], + method.parameters[0], + ); + strictEqual(method.pagingMetadata.continuationTokenResponseSegments?.length, 1); + strictEqual(method.operation.responses[0].type?.kind, "model"); + strictEqual( + method.pagingMetadata.continuationTokenResponseSegments?.[0], + method.operation.responses[0].headers[0], + ); + }); + + it("continuation token in response header and body parameter", async () => { + await runner.compileWithBuiltInService(` + @list + op test(@continuationToken token?: string): ListTestResult; + model ListTestResult { + @pageItems + items: Test[]; + @continuationToken + @header + nextToken?: string; + } + model Test { + prop: string; + } + `); + const sdkPackage = runner.context.sdkPackage; + const method = getServiceMethodOfClient(sdkPackage); + strictEqual(method.name, "test"); + strictEqual(method.kind, "paging"); + strictEqual(method.pagingMetadata.continuationTokenParameterSegments?.length, 1); + strictEqual( + method.pagingMetadata.continuationTokenParameterSegments?.[0], + method.parameters[0], + ); + strictEqual(method.pagingMetadata.continuationTokenResponseSegments?.length, 1); + strictEqual(method.operation.responses[0].type?.kind, "model"); + strictEqual( + method.pagingMetadata.continuationTokenResponseSegments?.[0], + method.operation.responses[0].headers[0], + ); + }); + + it("continuation token with @override", async () => { + await runner.compileWithBuiltInService(` + @list + op test(...Options): ListTestResult; + + model Options { + @continuationToken + @query + token?: string; + } + + @list + op customizedOp(options: Options): ListTestResult; + @@override(test, customizedOp); + + model ListTestResult { + @pageItems + items: Test[]; + @continuationToken + nextToken?: string; + } + model Test { + prop: string; + } + `); + const sdkPackage = runner.context.sdkPackage; + const method = getServiceMethodOfClient(sdkPackage); + strictEqual(method.name, "test"); + strictEqual(method.kind, "paging"); + strictEqual(method.pagingMetadata.continuationTokenParameterSegments?.length, 2); + strictEqual( + method.pagingMetadata.continuationTokenParameterSegments?.[0], + method.parameters[0], + ); + strictEqual(method.parameters[0].type.kind, "model"); + strictEqual( + method.pagingMetadata.continuationTokenParameterSegments?.[1], + method.parameters[0].type.properties[0], + ); + strictEqual( + method.operation.parameters[0].correspondingMethodParams[0], + method.parameters[0].type.properties[0], + ); + strictEqual(method.pagingMetadata.continuationTokenResponseSegments?.length, 1); + strictEqual(method.operation.responses[0].type?.kind, "model"); + strictEqual( + method.pagingMetadata.continuationTokenResponseSegments?.[0], + method.operation.responses[0].type.properties[1], + ); + }); + }); + + it("getPropertySegmentsFromModelOrParameters test for nested case", async () => { + await runner.compileWithBuiltInService(` + op test(): Test; + model Test { + a: { + b: { + a: string; + }; + }; + b: { + d: string; + }; + } + `); + const testModel = runner.context.sdkPackage.models[0]; + const aProperty = testModel.properties[0]; + const bProperty = testModel.properties[1]; + strictEqual(aProperty.type.kind, "model"); + const aBProperty = aProperty.type.properties[0]; + strictEqual(aBProperty.type.kind, "model"); + const aBAProperty = aBProperty.type.properties[0]; + strictEqual(bProperty.type.kind, "model"); + const bDProperty = bProperty.type.properties[0]; + + deepStrictEqual( + getPropertySegmentsFromModelOrParameters(testModel, (p) => p === aBAProperty), + [aProperty, aBProperty, aBAProperty], + ); + deepStrictEqual( + getPropertySegmentsFromModelOrParameters(testModel, (p) => p === bDProperty), + [bProperty, bDProperty], + ); + }); + + it("getPropertySegmentsFromModelOrParameters test for nested case of parameter", async () => { + await runner.compileWithBuiltInService(` + op test(param: Test): Test; + model Test { + a: { + b: { + a: string; + }; + }; + b: { + d: string; + }; + } + `); + const testModel = runner.context.sdkPackage.models[0]; + const aProperty = testModel.properties[0]; + const bProperty = testModel.properties[1]; + strictEqual(aProperty.type.kind, "model"); + const aBProperty = aProperty.type.properties[0]; + strictEqual(aBProperty.type.kind, "model"); + const aBAProperty = aBProperty.type.properties[0]; + strictEqual(bProperty.type.kind, "model"); + const bDProperty = bProperty.type.properties[0]; + + const parameters = runner.context.sdkPackage.clients[0].methods[0] + .parameters as SdkMethodParameter[]; + deepStrictEqual( + getPropertySegmentsFromModelOrParameters(parameters, (p) => p === aBAProperty), + [parameters[0], aProperty, aBProperty, aBAProperty], + ); + deepStrictEqual( + getPropertySegmentsFromModelOrParameters(parameters, (p) => p === bDProperty), + [parameters[0], bProperty, bDProperty], + ); }); }); diff --git a/packages/typespec-client-generator-core/test/packages/parameters.test.ts b/packages/typespec-client-generator-core/test/packages/parameters.test.ts index 56ed0bbb3f..656bb37b76 100644 --- a/packages/typespec-client-generator-core/test/packages/parameters.test.ts +++ b/packages/typespec-client-generator-core/test/packages/parameters.test.ts @@ -499,21 +499,21 @@ describe("typespec-client-generator-core: parameters", () => { strictEqual(methodParam.type.properties.length, 3); const model = methodParam.type; - strictEqual(model.properties[0].kind, "header"); + strictEqual(model.properties[0].kind, "property"); strictEqual(model.properties[0].name, "header"); strictEqual(model.properties[0].optional, false); strictEqual(model.properties[0].onClient, false); strictEqual(model.properties[0].isApiVersionParam, false); strictEqual(model.properties[0].type.kind, "string"); - strictEqual(model.properties[1].kind, "query"); + strictEqual(model.properties[1].kind, "property"); strictEqual(model.properties[1].name, "query"); strictEqual(model.properties[1].optional, false); strictEqual(model.properties[1].onClient, false); strictEqual(model.properties[1].isApiVersionParam, false); strictEqual(model.properties[1].type.kind, "string"); - strictEqual(model.properties[2].kind, "body"); + strictEqual(model.properties[2].kind, "property"); strictEqual(model.properties[2].name, "body"); strictEqual(model.properties[2].optional, false); strictEqual(model.properties[2].onClient, false); diff --git a/packages/typespec-client-generator-core/test/packages/responses.test.ts b/packages/typespec-client-generator-core/test/packages/responses.test.ts index 9194dba2c2..89cfb03398 100644 --- a/packages/typespec-client-generator-core/test/packages/responses.test.ts +++ b/packages/typespec-client-generator-core/test/packages/responses.test.ts @@ -193,7 +193,6 @@ describe("typespec-client-generator-core: responses", () => { sdkPackage.models.find((x) => x.name === "Widget"), ); strictEqual(methodResponseType.properties.length, 2); - strictEqual(methodResponseType.properties.filter((x) => x.kind === "header").length, 1); }); it("Headers and body with null", async () => { @@ -319,4 +318,18 @@ describe("typespec-client-generator-core: responses", () => { models[0], ); }); + + it("rename for response header", async function () { + await runner.compileWithBuiltInService(` + model Test{ + prop: string; + } + op get(): {@header @clientName("xRename") x: string}; + `); + const sdkPackage = runner.context.sdkPackage; + const method = sdkPackage.clients[0].methods[0] as SdkServiceMethod; + const header = method.operation.responses[0].headers[0]; + strictEqual(header.serializedName, "x"); + strictEqual(header.name, "xRename"); + }); }); diff --git a/packages/typespec-client-generator-core/test/public-utils.test.ts b/packages/typespec-client-generator-core/test/public-utils.test.ts index 8cb72b208a..e0d971a8cc 100644 --- a/packages/typespec-client-generator-core/test/public-utils.test.ts +++ b/packages/typespec-client-generator-core/test/public-utils.test.ts @@ -1693,7 +1693,7 @@ describe("typespec-client-generator-core: public-utils", () => { } `)) as { TestModel: Model }; - runner.context.generatedNames?.clear(); + runner.context.__generatedNames?.clear(); const name = getGeneratedName( runner.context, [...TestModel.properties.values()][0].type as Model, diff --git a/packages/typespec-client-generator-core/test/types/model-types.test.ts b/packages/typespec-client-generator-core/test/types/model-types.test.ts index 38ca6a2af9..925ea96b19 100644 --- a/packages/typespec-client-generator-core/test/types/model-types.test.ts +++ b/packages/typespec-client-generator-core/test/types/model-types.test.ts @@ -781,7 +781,7 @@ describe("typespec-client-generator-core: model types", () => { strictEqual(models[0].name, "User"); strictEqual(models[0].crossLanguageDefinitionId, "My.Service.User"); - for (const [type, sdkType] of runner.context.referencedTypeMap?.entries() ?? []) { + for (const [type, sdkType] of runner.context.__referencedTypeCache?.entries() ?? []) { if (isAzureCoreTspModel(type)) { ok(sdkType.usage !== UsageFlags.None); } @@ -1057,7 +1057,7 @@ describe("typespec-client-generator-core: model types", () => { strictEqual(fish?.properties[0].kind, "property"); strictEqual(fish?.properties[0].serializationOptions.json?.name, "kind"); - const salmon = Array.from(runner.context.referencedTypeMap?.values() ?? []).find( + const salmon = Array.from(runner.context.__referencedTypeCache?.values() ?? []).find( (x) => x.kind === "model" && x.name === "Salmon", ) as SdkModelType; strictEqual(salmon?.serializationOptions.json, undefined); @@ -1116,7 +1116,7 @@ describe("typespec-client-generator-core: model types", () => { strictEqual(fish?.properties[0].kind, "property"); strictEqual(fish?.properties[0].serializationOptions.json?.name, "kind"); - const types = Array.from(runner.context.referencedTypeMap?.values() ?? []); + const types = Array.from(runner.context.__referencedTypeCache?.values() ?? []); const shark = types.find((x) => x.kind === "model" && x.name === "Shark") as SdkModelType; strictEqual(shark?.serializationOptions.json, undefined); @@ -1805,7 +1805,7 @@ describe("typespec-client-generator-core: model types", () => { strictEqual(inputModel.properties.length, 1); const nameProperty = inputModel.properties[0]; strictEqual(nameProperty.name, "name"); - strictEqual(nameProperty.kind, "header"); + strictEqual(nameProperty.kind, "property"); ok(nameProperty.visibility); strictEqual(nameProperty.visibility.length, 1); strictEqual(nameProperty.visibility[0], Visibility.Read);