From 4a3e4282757786cf5381d94250b546d37ae6d530 Mon Sep 17 00:00:00 2001 From: tadelesh Date: Wed, 12 Feb 2025 17:45:33 +0800 Subject: [PATCH 01/12] support continuation token --- .../src/context.ts | 6 +- .../src/http.ts | 158 +++++------ .../src/interfaces.ts | 12 +- .../src/internal-utils.ts | 8 + .../src/package.ts | 160 +++++++++-- .../src/types.ts | 9 +- .../test/package.test.ts | 2 +- .../test/packages/paged-operation.test.ts | 255 +++++++++++++++++- .../test/packages/parameters.test.ts | 6 +- .../test/packages/responses.test.ts | 1 - .../test/types/model-types.test.ts | 2 +- 11 files changed, 506 insertions(+), 113 deletions(-) diff --git a/packages/typespec-client-generator-core/src/context.ts b/packages/typespec-client-generator-core/src/context.ts index ce54f7b323..ecd863ce14 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, @@ -44,7 +46,9 @@ export function createTCGCContext(program: Program, emitterName?: string): TCGCC __pagedResultSet: new Set(), referencedTypeMap: new Map(), httpOperationCache: new Map(), - referencedPropertyMap: new Map(), + modelPropertyMap: new Map(), + methodParameterMap: new Map(), + httpParameterMap: new Map(), }; } diff --git a/packages/typespec-client-generator-core/src/http.ts b/packages/typespec-client-generator-core/src/http.ts index 904b29f2d3..fd57d8a863 100644 --- a/packages/typespec-client-generator-core/src/http.ts +++ b/packages/typespec-client-generator-core/src/http.ts @@ -337,84 +337,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.httpParameterMap?.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.httpParameterMap.set(param, parameter); } - return diagnostics.wrap({ - ...headerQueryBase, - kind: "header", - serializedName: getHeaderFieldName(program, param) ?? base.name, - }); + return diagnostics.wrap(parameter); } function getSdkHttpResponseAndExceptions( diff --git a/packages/typespec-client-generator-core/src/interfaces.ts b/packages/typespec-client-generator-core/src/interfaces.ts index f93a00ae96..f590dc50b2 100644 --- a/packages/typespec-client-generator-core/src/interfaces.ts +++ b/packages/typespec-client-generator-core/src/interfaces.ts @@ -40,7 +40,9 @@ export interface TCGCContext { flattenUnionAsEnum?: boolean; arm?: boolean; referencedTypeMap: Map; - referencedPropertyMap: Map; + modelPropertyMap: Map; + methodParameterMap: Map; + httpParameterMap: Map; generatedNames?: Map; httpOperationCache: Map; __clientToParameters: Map; @@ -744,10 +746,14 @@ export interface SdkBasicServiceMethod diff --git a/packages/typespec-client-generator-core/src/internal-utils.ts b/packages/typespec-client-generator-core/src/internal-utils.ts index 3f27ef2574..34503c59d3 100644 --- a/packages/typespec-client-generator-core/src/internal-utils.ts +++ b/packages/typespec-client-generator-core/src/internal-utils.ts @@ -591,3 +591,11 @@ export function hasNoneVisibility(context: TCGCContext, type: ModelProperty): bo const visibility = getVisibilityForClass(context.program, type, lifecycle); return visibility.size === 0; } + +export function updateReferencedPropertyMap( + context: TCGCContext, + type: ModelProperty, + sdkType: SdkModelPropertyType, +) { + context.modelPropertyMap.set(type, sdkType); +} diff --git a/packages/typespec-client-generator-core/src/package.ts b/packages/typespec-client-generator-core/src/package.ts index 496542b56a..51f81ea625 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, @@ -151,7 +151,11 @@ function getSdkPagingServiceMethod p === pagingOperation.output.pageItems.property, ); - const nextLinkPath = pagingOperation.output.nextLink - ? getPropertyPathFromModel( - context, - responseType?.__raw, - (p) => p === pagingOperation.output.nextLink!.property, - ) - : undefined; + const pageItemsProperty = diagnostics.pipe( + getSdkModelPropertyType(context, pagingOperation.output.pageItems.property), + ); + basic.response.resultSegments = getPropertySegmentsFromModelOrParameters( + responseType, + (p) => p === pageItemsProperty, + ); + + let nextLinkPath = undefined; + let nextLinkSegments = undefined; + if (pagingOperation.output.nextLink) { + nextLinkPath = getPropertyPathFromModel( + context, + responseType?.__raw, + (p) => p === pagingOperation.output.nextLink!.property, + ); + const nextLinkProperty = diagnostics.pipe( + getSdkModelPropertyType(context, pagingOperation.output.nextLink?.property), + ); + nextLinkSegments = getPropertySegmentsFromModelOrParameters( + responseType, + (p) => p === nextLinkProperty, + ); + } + + let continuationTokenParameterSegments = undefined; + let continuationTokenResponseSegments = undefined; + if (pagingOperation.input.continuationToken) { + // find direct parameter first, then find nested property if it's not a direct parameter + const directParameter = operation.parameters.properties.get( + pagingOperation.input.continuationToken.property.name, + ); + if (directParameter === pagingOperation.input.continuationToken.property) { + continuationTokenParameterSegments = [ + diagnostics.pipe(getSdkMethodParameter(context, directParameter, operation)), + ]; + } else { + const continuationTokenParameter = diagnostics.pipe( + getSdkModelPropertyType(context, pagingOperation.input.continuationToken.property), + ); + continuationTokenParameterSegments = getPropertySegmentsFromModelOrParameters( + basic.parameters, + (p) => p === continuationTokenParameter, + ); + } + } + if (pagingOperation.output.continuationToken) { + if (isHeader(context.program, pagingOperation.output.continuationToken.property)) { + continuationTokenResponseSegments = basic.operation.responses + .map((r) => r.headers) + .flat() + .filter((h) => h.__raw === pagingOperation.output.continuationToken!.property); + } else { + const continuationTokenProperty = diagnostics.pipe( + getSdkModelPropertyType(context, pagingOperation.output.continuationToken.property), + ); + continuationTokenResponseSegments = getPropertySegmentsFromModelOrParameters( + responseType, + (p) => p === continuationTokenProperty, + ); + } + } context.__pagedResultSet.add(responseType); // tcgc will let all paging method return a list of items @@ -191,13 +250,20 @@ function getSdkPagingServiceMethod p === pageItemsProperty, + ); + + let nextLinkPath = undefined; + let nextLinkSegments = undefined; + if (pagedMetadata.nextLinkProperty) { + nextLinkPath = getPropertyPathFromSegment( + context, + pagedMetadata.modelType, + pagedMetadata?.nextLinkSegments, + ); + const nextLinkProperty = diagnostics.pipe( + getSdkModelPropertyType(context, pagedMetadata.nextLinkProperty), + ); + nextLinkSegments = getPropertySegmentsFromModelOrParameters( + responseType, + (p) => p === nextLinkProperty, + ); + } return diagnostics.wrap({ ...basic, __raw_paged_metadata: pagedMetadata, kind: "paging", - nextLinkPath: getPropertyPathFromSegment( - context, - pagedMetadata.modelType, - pagedMetadata?.nextLinkSegments, - ), + nextLinkPath, nextLinkOperation: pagedMetadata?.nextLinkOperation ? diagnostics.pipe( getSdkServiceOperation( @@ -245,6 +331,7 @@ function getSdkPagingServiceMethod boolean, +): SdkModelPropertyType[] | undefined { + const queue: { model: SdkModelType; path: SdkModelPropertyType[] }[] = []; + + 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, @@ -684,10 +801,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.methodParameterMap.get(type); + if (!parameter) { + parameter = { + ...diagnostics.pipe(getSdkModelPropertyType(context, type, operation)), + kind: "method", + }; + context.methodParameterMap.set(type, parameter); + } + return diagnostics.wrap(parameter as SdkMethodParameter); } function getSdkMethods( diff --git a/packages/typespec-client-generator-core/src/types.ts b/packages/typespec-client-generator-core/src/types.ts index 309abf43ee..b995d2b39c 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( @@ -1186,7 +1186,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); } @@ -1310,7 +1310,7 @@ export function getSdkModelPropertyType( ): [SdkModelPropertyType, readonly Diagnostic[]] { const diagnostics = createDiagnosticCollector(); - let property = context.referencedPropertyMap?.get(type); + let property = context.modelPropertyMap?.get(type); if (!property) { const clientParams = operation @@ -1322,7 +1322,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", @@ -1386,7 +1385,7 @@ function updateReferencedPropertyMap( if (sdkType.kind !== "property") { return; } - context.referencedPropertyMap.set(type, sdkType); + context.modelPropertyMap.set(type, sdkType); } function updateReferencedTypeMap(context: TCGCContext, type: Type, sdkType: SdkType) { diff --git a/packages/typespec-client-generator-core/test/package.test.ts b/packages/typespec-client-generator-core/test/package.test.ts index bfbc873eaa..3d28a458b4 100644 --- a/packages/typespec-client-generator-core/test/package.test.ts +++ b/packages/typespec-client-generator-core/test/package.test.ts @@ -948,7 +948,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..ce1b8201a4 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"; @@ -208,4 +212,251 @@ describe("typespec-client-generator-core: paged operation", () => { strictEqual(response.kind, "method"); strictEqual(response.resultPath, "values"); }); + + 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.continuationTokenParameterSegments?.length, 1); + strictEqual(method.continuationTokenParameterSegments?.[0], method.parameters[0]); + strictEqual( + method.operation.parameters[0].correspondingMethodParams[0], + method.parameters[0], + ); + strictEqual(method.continuationTokenResponseSegments?.length, 1); + strictEqual(method.operation.responses[0].type?.kind, "model"); + strictEqual( + method.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.continuationTokenParameterSegments?.length, 1); + strictEqual(method.continuationTokenParameterSegments?.[0], method.parameters[0]); + strictEqual(method.continuationTokenResponseSegments?.length, 1); + strictEqual(method.operation.responses[0].type?.kind, "model"); + strictEqual( + method.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.continuationTokenParameterSegments?.length, 1); + strictEqual(method.continuationTokenParameterSegments?.[0], method.parameters[0]); + strictEqual(method.continuationTokenResponseSegments?.length, 1); + strictEqual(method.operation.responses[0].type?.kind, "model"); + strictEqual( + method.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.continuationTokenParameterSegments?.length, 1); + strictEqual(method.continuationTokenParameterSegments?.[0], method.parameters[0]); + strictEqual(method.continuationTokenResponseSegments?.length, 1); + strictEqual(method.operation.responses[0].type?.kind, "model"); + strictEqual( + method.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.continuationTokenParameterSegments?.length, 1); + strictEqual(method.continuationTokenParameterSegments?.[0], method.parameters[0]); + strictEqual(method.continuationTokenResponseSegments?.length, 1); + strictEqual(method.operation.responses[0].type?.kind, "model"); + strictEqual( + method.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.continuationTokenParameterSegments?.length, 1); + strictEqual(method.continuationTokenParameterSegments?.[0], method.parameters[0]); + strictEqual(method.continuationTokenResponseSegments?.length, 1); + strictEqual(method.operation.responses[0].type?.kind, "model"); + strictEqual( + method.continuationTokenResponseSegments?.[0], + method.operation.responses[0].headers[0], + ); + }); + }); + + 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..bd6c84d50f 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 () => { 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..e7ed267af0 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 @@ -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); From 64eb2f4fc33bd11b7e601a4980d7dd27d7907396 Mon Sep 17 00:00:00 2001 From: tadelesh Date: Wed, 12 Feb 2025 17:56:04 +0800 Subject: [PATCH 02/12] add test for `nextLinkSegments` --- .../typespec-client-generator-core/src/package.ts | 12 ++++++++++++ .../test/packages/paged-operation.test.ts | 14 ++++++++++++++ 2 files changed, 26 insertions(+) diff --git a/packages/typespec-client-generator-core/src/package.ts b/packages/typespec-client-generator-core/src/package.ts index 51f81ea625..9a83004ddf 100644 --- a/packages/typespec-client-generator-core/src/package.ts +++ b/packages/typespec-client-generator-core/src/package.ts @@ -342,6 +342,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); @@ -375,6 +380,13 @@ export function getPropertySegmentsFromModelOrParameters( ): 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]; 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 ce1b8201a4..3405ab3f32 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 @@ -42,6 +42,8 @@ describe("typespec-client-generator-core: paged operation", () => { strictEqual(method.name, "test"); strictEqual(method.kind, "paging"); strictEqual(method.nextLinkPath, "nextLink"); + strictEqual(method.nextLinkSegments?.length, 1); + strictEqual(method.nextLinkSegments[0], sdkPackage.models[0].properties[1]); const response = method.response; strictEqual(response.kind, "method"); @@ -67,6 +69,8 @@ describe("typespec-client-generator-core: paged operation", () => { strictEqual(method.name, "test"); strictEqual(method.kind, "paging"); strictEqual(method.nextLinkPath, "next"); + strictEqual(method.nextLinkSegments?.length, 1); + strictEqual(method.nextLinkSegments[0], sdkPackage.models[0].properties[1]); const response = method.response; strictEqual(response.kind, "method"); @@ -92,6 +96,8 @@ describe("typespec-client-generator-core: paged operation", () => { strictEqual(method.name, "test"); strictEqual(method.kind, "paging"); strictEqual(method.nextLinkPath, "next"); + strictEqual(method.nextLinkSegments?.length, 1); + strictEqual(method.nextLinkSegments[0], sdkPackage.models[0].properties[1]); const response = method.response; strictEqual(response.kind, "method"); @@ -119,6 +125,8 @@ describe("typespec-client-generator-core: paged operation", () => { strictEqual(method.name, "test"); strictEqual(method.kind, "paging"); strictEqual(method.nextLinkPath, "nextLink"); + strictEqual(method.nextLinkSegments?.length, 1); + strictEqual(method.nextLinkSegments[0], sdkPackage.models[0].properties[1]); const response = method.response; strictEqual(response.kind, "method"); @@ -149,6 +157,10 @@ describe("typespec-client-generator-core: paged operation", () => { strictEqual(method.name, "test"); strictEqual(method.kind, "paging"); strictEqual(method.nextLinkPath, "pagination.nextLink"); + strictEqual(method.nextLinkSegments?.length, 2); + strictEqual(method.nextLinkSegments[0], sdkPackage.models[0].properties[1]); + strictEqual(sdkPackage.models[0].properties[1].type.kind, "model"); + strictEqual(method.nextLinkSegments[1], sdkPackage.models[0].properties[1].type.properties[0]); const response = method.response; strictEqual(response.kind, "method"); @@ -207,6 +219,8 @@ describe("typespec-client-generator-core: paged operation", () => { strictEqual(method.name, "test"); strictEqual(method.kind, "paging"); strictEqual(method.nextLinkPath, "nextLink"); + strictEqual(method.nextLinkSegments?.length, 1); + strictEqual(method.nextLinkSegments[0], sdkPackage.models[1].properties[1]); const response = method.response; strictEqual(response.kind, "method"); From 9d4ca709e8b53bc6b6e0874dad8f62d680e3f305 Mon Sep 17 00:00:00 2001 From: tadelesh Date: Wed, 12 Feb 2025 17:58:16 +0800 Subject: [PATCH 03/12] changeset --- .chronus/changes/continuation_token-2025-1-12-17-57-14.md | 7 +++++++ .chronus/changes/continuation_token-2025-1-12-17-58-11.md | 7 +++++++ 2 files changed, 14 insertions(+) create mode 100644 .chronus/changes/continuation_token-2025-1-12-17-57-14.md create mode 100644 .chronus/changes/continuation_token-2025-1-12-17-58-11.md 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..7c36080208 --- /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 `continuationTokenParameterSegments` and `continuationTokenResponseSegments` to `SdkPagingServiceMethodOptions` 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..07497d077c --- /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 `nextLinkPath` in `SdkPagingServiceMethodOptions`. Use `nextLinkSegments` instead. \ No newline at end of file From b9d28665fc09e8d119a9973cd7621801c0e97a4a Mon Sep 17 00:00:00 2001 From: tadelesh Date: Wed, 12 Feb 2025 18:00:15 +0800 Subject: [PATCH 04/12] fix --- .chronus/changes/continuation_token-2025-1-12-17-57-14.md | 2 +- .chronus/changes/continuation_token-2025-1-12-17-58-11.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) 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 index 7c36080208..375d6a38bb 100644 --- a/.chronus/changes/continuation_token-2025-1-12-17-57-14.md +++ b/.chronus/changes/continuation_token-2025-1-12-17-57-14.md @@ -4,4 +4,4 @@ packages: - "@azure-tools/typespec-client-generator-core" --- -Add `continuationTokenParameterSegments` and `continuationTokenResponseSegments` to `SdkPagingServiceMethodOptions` to indicate the mapping of continuation token parameter and response. \ No newline at end of file +Add `continuationTokenParameterSegments` and `continuationTokenResponseSegments` to `SdkPagingServiceMethodOptions` 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 index 07497d077c..0c4a5f4ced 100644 --- a/.chronus/changes/continuation_token-2025-1-12-17-58-11.md +++ b/.chronus/changes/continuation_token-2025-1-12-17-58-11.md @@ -4,4 +4,4 @@ packages: - "@azure-tools/typespec-client-generator-core" --- -Deprecate `nextLinkPath` in `SdkPagingServiceMethodOptions`. Use `nextLinkSegments` instead. \ No newline at end of file +Deprecate `nextLinkPath` in `SdkPagingServiceMethodOptions`. Use `nextLinkSegments` instead. \ No newline at end of file From d6e89dd5a9a0f5b73fca6a01753622885f4bbf7a Mon Sep 17 00:00:00 2001 From: tadelesh Date: Thu, 13 Feb 2025 16:21:49 +0800 Subject: [PATCH 05/12] remove useless code --- .../typespec-client-generator-core/src/internal-utils.ts | 8 -------- 1 file changed, 8 deletions(-) diff --git a/packages/typespec-client-generator-core/src/internal-utils.ts b/packages/typespec-client-generator-core/src/internal-utils.ts index 34503c59d3..3f27ef2574 100644 --- a/packages/typespec-client-generator-core/src/internal-utils.ts +++ b/packages/typespec-client-generator-core/src/internal-utils.ts @@ -591,11 +591,3 @@ export function hasNoneVisibility(context: TCGCContext, type: ModelProperty): bo const visibility = getVisibilityForClass(context.program, type, lifecycle); return visibility.size === 0; } - -export function updateReferencedPropertyMap( - context: TCGCContext, - type: ModelProperty, - sdkType: SdkModelPropertyType, -) { - context.modelPropertyMap.set(type, sdkType); -} From 1e014cdcd2b33eae1d62762e19ebec0e35a3bbc7 Mon Sep 17 00:00:00 2001 From: tadelesh Date: Fri, 14 Feb 2025 15:03:08 +0800 Subject: [PATCH 06/12] refine logic --- .../package.json | 4 +- .../src/http.ts | 17 ++--- .../src/interfaces.ts | 22 ++++--- .../src/internal-utils.ts | 7 +++ .../src/package.ts | 51 +++++---------- .../test/package.test.ts | 2 + .../test/packages/paged-operation.test.ts | 62 +++++++++++++++++++ .../test/packages/responses.test.ts | 14 +++++ 8 files changed, 118 insertions(+), 61 deletions(-) diff --git a/packages/typespec-client-generator-core/package.json b/packages/typespec-client-generator-core/package.json index 4b26567aed..b57e2f79fe 100644 --- a/packages/typespec-client-generator-core/package.json +++ b/packages/typespec-client-generator-core/package.json @@ -23,8 +23,8 @@ "exports": { ".": { "types": "./dist/src/index.d.ts", - "default": "./dist/src/index.js", - "typespec": "./lib/main.tsp" + "typespec": "./lib/main.tsp", + "default": "./dist/src/index.js" }, "./testing": { "types": "./dist/src/testing/index.d.ts", diff --git a/packages/typespec-client-generator-core/src/http.ts b/packages/typespec-client-generator-core/src/http.ts index fd57d8a863..618d83b08d 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, @@ -445,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)) { @@ -697,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 f590dc50b2..1af262514a 100644 --- a/packages/typespec-client-generator-core/src/interfaces.ts +++ b/packages/typespec-client-generator-core/src/interfaces.ts @@ -559,7 +559,8 @@ export type SdkModelPropertyType = | SdkPathParameter | SdkBodyParameter | SdkHeaderParameter - | SdkCookieParameter; + | SdkCookieParameter + | SdkServiceResponseHeader; export interface MultipartOptions { name: string; @@ -646,27 +647,28 @@ export type SdkHttpParameter = | SdkPathParameter | SdkBodyParameter | SdkHeaderParameter - | SdkCookieParameter; + | SdkCookieParameter + | SdkServiceResponseHeader; 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[]; } @@ -753,7 +755,7 @@ interface SdkPagingServiceMethodOptions { nextLinkSegments?: SdkModelPropertyType[]; nextLinkOperation?: SdkServiceOperation; continuationTokenParameterSegments?: SdkModelPropertyType[]; - continuationTokenResponseSegments?: SdkModelPropertyType[] | SdkServiceResponseHeader[]; + continuationTokenResponseSegments?: SdkModelPropertyType[]; } export interface SdkPagingServiceMethod diff --git a/packages/typespec-client-generator-core/src/internal-utils.ts b/packages/typespec-client-generator-core/src/internal-utils.ts index 3f27ef2574..4b245c29bc 100644 --- a/packages/typespec-client-generator-core/src/internal-utils.ts +++ b/packages/typespec-client-generator-core/src/internal-utils.ts @@ -591,3 +591,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 9a83004ddf..c2c86420a3 100644 --- a/packages/typespec-client-generator-core/src/package.ts +++ b/packages/typespec-client-generator-core/src/package.ts @@ -64,6 +64,7 @@ import { import { createGeneratedName, filterApiVersionsWithDecorators, + findRootSourceProperty, getAllResponseBodiesAndNonBodyExists, getAvailableApiVersions, getClientNamespaceStringHelper, @@ -172,17 +173,15 @@ function getSdkPagingServiceMethod p === pagingOperation.output.pageItems.property, ); - const pageItemsProperty = diagnostics.pipe( - getSdkModelPropertyType(context, pagingOperation.output.pageItems.property), - ); basic.response.resultSegments = getPropertySegmentsFromModelOrParameters( responseType, - (p) => p === pageItemsProperty, + (p) => p.__raw === pagingOperation.output.pageItems.property, ); let nextLinkPath = undefined; @@ -193,35 +192,22 @@ function getSdkPagingServiceMethod p === pagingOperation.output.nextLink!.property, ); - const nextLinkProperty = diagnostics.pipe( - getSdkModelPropertyType(context, pagingOperation.output.nextLink?.property), - ); nextLinkSegments = getPropertySegmentsFromModelOrParameters( responseType, - (p) => p === nextLinkProperty, + (p) => p.__raw === pagingOperation.output.nextLink?.property, ); } let continuationTokenParameterSegments = undefined; let continuationTokenResponseSegments = undefined; if (pagingOperation.input.continuationToken) { - // find direct parameter first, then find nested property if it's not a direct parameter - const directParameter = operation.parameters.properties.get( - pagingOperation.input.continuationToken.property.name, + continuationTokenParameterSegments = getPropertySegmentsFromModelOrParameters( + basic.parameters, + (p) => + p.__raw?.kind === "ModelProperty" && + findRootSourceProperty(p.__raw) === + findRootSourceProperty(pagingOperation.input.continuationToken!.property), ); - if (directParameter === pagingOperation.input.continuationToken.property) { - continuationTokenParameterSegments = [ - diagnostics.pipe(getSdkMethodParameter(context, directParameter, operation)), - ]; - } else { - const continuationTokenParameter = diagnostics.pipe( - getSdkModelPropertyType(context, pagingOperation.input.continuationToken.property), - ); - continuationTokenParameterSegments = getPropertySegmentsFromModelOrParameters( - basic.parameters, - (p) => p === continuationTokenParameter, - ); - } } if (pagingOperation.output.continuationToken) { if (isHeader(context.program, pagingOperation.output.continuationToken.property)) { @@ -230,12 +216,9 @@ function getSdkPagingServiceMethod h.__raw === pagingOperation.output.continuationToken!.property); } else { - const continuationTokenProperty = diagnostics.pipe( - getSdkModelPropertyType(context, pagingOperation.output.continuationToken.property), - ); continuationTokenResponseSegments = getPropertySegmentsFromModelOrParameters( responseType, - (p) => p === continuationTokenProperty, + (p) => p.__raw === pagingOperation.output.continuationToken!.property, ); } } @@ -287,17 +270,15 @@ function getSdkPagingServiceMethod p === pageItemsProperty, + (p) => p.__raw === pagedMetadata.itemsProperty, ); let nextLinkPath = undefined; @@ -308,12 +289,9 @@ function getSdkPagingServiceMethod p === nextLinkProperty, + (p) => p.__raw === pagedMetadata.nextLinkProperty, ); } @@ -450,7 +428,6 @@ function getSdkLroServiceMethod( // eslint-disable-next-line @typescript-eslint/no-deprecated basicServiceMethod.response.resultPath = metadata.finalResponse?.resultPath; - basicServiceMethod.response.resultSegments = metadata.finalResponse?.resultSegments; return diagnostics.wrap({ diff --git a/packages/typespec-client-generator-core/test/package.test.ts b/packages/typespec-client-generator-core/test/package.test.ts index 3d28a458b4..341c3c3bf1 100644 --- a/packages/typespec-client-generator-core/test/package.test.ts +++ b/packages/typespec-client-generator-core/test/package.test.ts @@ -813,6 +813,8 @@ 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({ 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 3405ab3f32..d0571ac738 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 @@ -48,6 +48,8 @@ describe("typespec-client-generator-core: paged operation", () => { 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 () => { @@ -75,6 +77,8 @@ describe("typespec-client-generator-core: paged operation", () => { 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 () => { @@ -102,6 +106,8 @@ describe("typespec-client-generator-core: paged operation", () => { 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 () => { @@ -131,6 +137,8 @@ describe("typespec-client-generator-core: paged operation", () => { 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 @@ -165,6 +173,10 @@ describe("typespec-client-generator-core: paged operation", () => { 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 () => { @@ -225,6 +237,8 @@ describe("typespec-client-generator-core: paged operation", () => { 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", () => { @@ -402,6 +416,54 @@ describe("typespec-client-generator-core: paged operation", () => { 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.continuationTokenParameterSegments?.length, 2); + strictEqual(method.continuationTokenParameterSegments?.[0], method.parameters[0]); + strictEqual(method.parameters[0].type.kind, "model"); + strictEqual( + method.continuationTokenParameterSegments?.[1], + method.parameters[0].type.properties[0], + ); + strictEqual( + method.operation.parameters[0].correspondingMethodParams[0], + method.parameters[0].type.properties[0], + ); + strictEqual(method.continuationTokenResponseSegments?.length, 1); + strictEqual(method.operation.responses[0].type?.kind, "model"); + strictEqual( + method.continuationTokenResponseSegments?.[0], + method.operation.responses[0].type.properties[1], + ); + }); }); it("getPropertySegmentsFromModelOrParameters test for nested case", async () => { 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 bd6c84d50f..89cfb03398 100644 --- a/packages/typespec-client-generator-core/test/packages/responses.test.ts +++ b/packages/typespec-client-generator-core/test/packages/responses.test.ts @@ -318,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"); + }); }); From 2b1f957b5d20f162ba306a0e00c8c92d75b16049 Mon Sep 17 00:00:00 2001 From: tadelesh Date: Fri, 14 Feb 2025 15:05:57 +0800 Subject: [PATCH 07/12] format --- packages/typespec-client-generator-core/test/package.test.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/packages/typespec-client-generator-core/test/package.test.ts b/packages/typespec-client-generator-core/test/package.test.ts index 341c3c3bf1..2afee4c3cf 100644 --- a/packages/typespec-client-generator-core/test/package.test.ts +++ b/packages/typespec-client-generator-core/test/package.test.ts @@ -814,7 +814,10 @@ describe("typespec-client-generator-core: package", () => { 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]); + strictEqual( + createOrUpdate.response.resultSegments[0], + createOrUpdate.lroMetadata.finalResponse?.envelopeResult.properties[3], + ); }); it("lro delete", async () => { const runnerWithCore = await createSdkTestRunner({ From 79d2aad8a7fa156c9df29f26941cd80054a352d0 Mon Sep 17 00:00:00 2001 From: tadelesh Date: Fri, 14 Feb 2025 16:21:47 +0800 Subject: [PATCH 08/12] refine --- .chronus/changes/continuation_token-2025-1-14-16-15-29.md | 7 +++++++ .chronus/changes/continuation_token-2025-1-14-16-17-22.md | 7 +++++++ packages/typespec-client-generator-core/src/interfaces.ts | 3 +-- 3 files changed, 15 insertions(+), 2 deletions(-) create mode 100644 .chronus/changes/continuation_token-2025-1-14-16-15-29.md create mode 100644 .chronus/changes/continuation_token-2025-1-14-16-17-22.md 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..43959e6d8a --- /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..0b67b17a23 --- /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/packages/typespec-client-generator-core/src/interfaces.ts b/packages/typespec-client-generator-core/src/interfaces.ts index 1af262514a..d46db56b8a 100644 --- a/packages/typespec-client-generator-core/src/interfaces.ts +++ b/packages/typespec-client-generator-core/src/interfaces.ts @@ -647,8 +647,7 @@ export type SdkHttpParameter = | SdkPathParameter | SdkBodyParameter | SdkHeaderParameter - | SdkCookieParameter - | SdkServiceResponseHeader; + | SdkCookieParameter; export interface SdkMethodParameter extends SdkModelPropertyTypeBase { kind: "method"; From 2bcb6b9360ec3f8d24e027664bbe513cbc8ab9cf Mon Sep 17 00:00:00 2001 From: tadelesh Date: Fri, 14 Feb 2025 16:34:18 +0800 Subject: [PATCH 09/12] renaming --- .../src/package.ts | 42 +++++++++---------- 1 file changed, 21 insertions(+), 21 deletions(-) diff --git a/packages/typespec-client-generator-core/src/package.ts b/packages/typespec-client-generator-core/src/package.ts index c2c86420a3..ec65d7524e 100644 --- a/packages/typespec-client-generator-core/src/package.ts +++ b/packages/typespec-client-generator-core/src/package.ts @@ -138,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; } @@ -168,18 +168,18 @@ function getSdkPagingServiceMethod p === pagingOperation.output.pageItems.property, ); - basic.response.resultSegments = getPropertySegmentsFromModelOrParameters( + baseServiceMethod.response.resultSegments = getPropertySegmentsFromModelOrParameters( responseType, (p) => p.__raw === pagingOperation.output.pageItems.property, ); @@ -202,7 +202,7 @@ function getSdkPagingServiceMethod p.__raw?.kind === "ModelProperty" && findRootSourceProperty(p.__raw) === @@ -211,7 +211,7 @@ function getSdkPagingServiceMethod r.headers) .flat() .filter((h) => h.__raw === pagingOperation.output.continuationToken!.property); @@ -225,12 +225,12 @@ function getSdkPagingServiceMethod p.__raw === pagedMetadata.itemsProperty, ); @@ -296,7 +296,7 @@ function getSdkPagingServiceMethod( context, pagedMetadata.nextLinkOperation, - basic.parameters, + baseServiceMethod.parameters, ), ) : undefined, @@ -420,18 +420,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, @@ -439,7 +439,7 @@ function getSdkLroServiceMethod( getSdkServiceOperation( context, metadata.__raw.operation, - basicServiceMethod.parameters, + baseServiceMethod.parameters, ), ), }); From e6af40485ccdb92693bc4d1d271bb32fa0f6b566 Mon Sep 17 00:00:00 2001 From: tadelesh Date: Mon, 17 Feb 2025 16:22:28 +0800 Subject: [PATCH 10/12] standardize context parameter name --- .../src/context.ts | 15 +++++---- .../src/decorators.ts | 2 +- .../src/http.ts | 4 +-- .../src/interfaces.ts | 33 ++++++++++--------- .../src/package.ts | 4 +-- .../src/public-utils.ts | 22 +++++++------ .../src/types.ts | 28 ++++++++-------- .../test/public-utils.test.ts | 2 +- .../test/types/model-types.test.ts | 6 ++-- 9 files changed, 62 insertions(+), 54 deletions(-) diff --git a/packages/typespec-client-generator-core/src/context.ts b/packages/typespec-client-generator-core/src/context.ts index ecd863ce14..cf5b15e22d 100644 --- a/packages/typespec-client-generator-core/src/context.ts +++ b/packages/typespec-client-generator-core/src/context.ts @@ -37,18 +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(), - modelPropertyMap: new Map(), - methodParameterMap: new Map(), - httpParameterMap: 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 618d83b08d..a1935862d9 100644 --- a/packages/typespec-client-generator-core/src/http.ts +++ b/packages/typespec-client-generator-core/src/http.ts @@ -338,7 +338,7 @@ export function getSdkHttpParameter( location?: "path" | "query" | "header" | "body" | "cookie", ): [SdkHttpParameter, readonly Diagnostic[]] { const diagnostics = createDiagnosticCollector(); - let parameter = context.httpParameterMap?.get(param); + let parameter = context.__httpParameterCache?.get(param); if (!parameter) { const base = diagnostics.pipe(getSdkModelPropertyTypeBase(context, param, operation)); @@ -417,7 +417,7 @@ export function getSdkHttpParameter( serializedName: getHeaderFieldName(program, param) ?? base.name, }; } - context.httpParameterMap.set(param, parameter); + context.__httpParameterCache.set(param, parameter); } return diagnostics.wrap(parameter); } diff --git a/packages/typespec-client-generator-core/src/interfaces.ts b/packages/typespec-client-generator-core/src/interfaces.ts index d46db56b8a..899cc3422c 100644 --- a/packages/typespec-client-generator-core/src/interfaces.ts +++ b/packages/typespec-client-generator-core/src/interfaces.ts @@ -33,34 +33,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; - modelPropertyMap: Map; - methodParameterMap: Map; - httpParameterMap: 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; } diff --git a/packages/typespec-client-generator-core/src/package.ts b/packages/typespec-client-generator-core/src/package.ts index ec65d7524e..0731764494 100644 --- a/packages/typespec-client-generator-core/src/package.ts +++ b/packages/typespec-client-generator-core/src/package.ts @@ -790,13 +790,13 @@ function getSdkMethodParameter( operation: Operation, ): [SdkMethodParameter, readonly Diagnostic[]] { const diagnostics = createDiagnosticCollector(); - let parameter = context.methodParameterMap.get(type); + let parameter = context.__methodParameterCache.get(type); if (!parameter) { parameter = { ...diagnostics.pipe(getSdkModelPropertyType(context, type, operation)), kind: "method", }; - context.methodParameterMap.set(type, parameter); + context.__methodParameterCache.set(type, parameter); } return diagnostics.wrap(parameter as SdkMethodParameter); } 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 b0acc8144b..7218227da9 100644 --- a/packages/typespec-client-generator-core/src/types.ts +++ b/packages/typespec-client-generator-core/src/types.ts @@ -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) { @@ -799,7 +799,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); @@ -836,7 +836,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) { @@ -933,7 +933,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) { sdkType = { ...diagnostics.pipe(getSdkTypeBaseHelper(context, type, "enum")), @@ -1034,8 +1034,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; @@ -1376,7 +1376,7 @@ export function getSdkModelPropertyType( ): [SdkModelPropertyType, readonly Diagnostic[]] { const diagnostics = createDiagnosticCollector(); - let property = context.modelPropertyMap?.get(type); + let property = context.__modelPropertyCache?.get(type); if (!property) { const clientParams = operation @@ -1451,7 +1451,7 @@ function updateReferencedPropertyMap( if (sdkType.kind !== "property") { return; } - context.modelPropertyMap.set(type, sdkType); + context.__modelPropertyCache.set(type, sdkType); } function updateReferencedTypeMap(context: TCGCContext, type: Type, sdkType: SdkType) { @@ -1463,7 +1463,7 @@ function updateReferencedTypeMap(context: TCGCContext, type: Type, sdkType: SdkT ) { return; } - context.referencedTypeMap?.set(type, sdkType); + context.__referencedTypeCache?.set(type, sdkType); } interface PropagationOptions { @@ -1790,13 +1790,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 })); @@ -1807,7 +1807,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 })); @@ -1817,7 +1817,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 && @@ -1851,7 +1851,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/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 e7ed267af0..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); From 4185787f3f011c8ca21545a7fa9d1eebe0ccf064 Mon Sep 17 00:00:00 2001 From: tadelesh Date: Tue, 18 Feb 2025 16:50:02 +0800 Subject: [PATCH 11/12] refactor --- .../continuation_token-2025-1-12-17-57-14.md | 2 +- .../continuation_token-2025-1-12-17-58-11.md | 2 +- .../continuation_token-2025-1-18-16-47-36.md | 7 ++ .../src/interfaces.ts | 32 +++++- .../src/package.ts | 25 +++- .../test/packages/paged-operation.test.ts | 108 +++++++++++------- 6 files changed, 123 insertions(+), 53 deletions(-) create mode 100644 .chronus/changes/continuation_token-2025-1-18-16-47-36.md 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 index 375d6a38bb..cf494827c0 100644 --- a/.chronus/changes/continuation_token-2025-1-12-17-57-14.md +++ b/.chronus/changes/continuation_token-2025-1-12-17-57-14.md @@ -4,4 +4,4 @@ packages: - "@azure-tools/typespec-client-generator-core" --- -Add `continuationTokenParameterSegments` and `continuationTokenResponseSegments` to `SdkPagingServiceMethodOptions` to indicate the mapping of continuation token parameter and response. \ No newline at end of file +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 index 0c4a5f4ced..a70a2a9319 100644 --- a/.chronus/changes/continuation_token-2025-1-12-17-58-11.md +++ b/.chronus/changes/continuation_token-2025-1-12-17-58-11.md @@ -4,4 +4,4 @@ packages: - "@azure-tools/typespec-client-generator-core" --- -Deprecate `nextLinkPath` in `SdkPagingServiceMethodOptions`. Use `nextLinkSegments` instead. \ No newline at end of file +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-18-16-47-36.md b/.chronus/changes/continuation_token-2025-1-18-16-47-36.md new file mode 100644 index 0000000000..962d9a6a54 --- /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/interfaces.ts b/packages/typespec-client-generator-core/src/interfaces.ts index 18afb26805..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, @@ -784,21 +785,42 @@ export interface SdkBasicServiceMethod { + /** + * @deprecated Use `pagingMetadata.__raw` instead. + */ __raw_paged_metadata?: PagedResultMetadata; /** - * @deprecated Use `nextLinkSegments` instead. + * @deprecated Use `pagingMetadata.nextLinkSegments` instead. */ nextLinkPath?: string; - nextLinkSegments?: SdkModelPropertyType[]; + /** + * @deprecated Use `pagingMetadata.nextLinkOperation` instead. + */ nextLinkOperation?: SdkServiceOperation; + 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"; } @@ -874,7 +896,7 @@ export interface SdkLroServiceMethod extends SdkServiceMethodBase, SdkLroServiceMethodOptions, - SdkPagingServiceMethodOptions { + SdkPagingServiceMethodOptions { kind: "lropaging"; } diff --git a/packages/typespec-client-generator-core/src/package.ts b/packages/typespec-client-generator-core/src/package.ts index d17afa09da..afd8240bc7 100644 --- a/packages/typespec-client-generator-core/src/package.ts +++ b/packages/typespec-client-generator-core/src/package.ts @@ -170,6 +170,7 @@ function getSdkPagingServiceMethod( + context, + pagedMetadata.nextLinkOperation, + client, + ), + ) + : undefined, + }, }); } 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 d0571ac738..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 @@ -42,8 +42,8 @@ describe("typespec-client-generator-core: paged operation", () => { strictEqual(method.name, "test"); strictEqual(method.kind, "paging"); strictEqual(method.nextLinkPath, "nextLink"); - strictEqual(method.nextLinkSegments?.length, 1); - strictEqual(method.nextLinkSegments[0], sdkPackage.models[0].properties[1]); + strictEqual(method.pagingMetadata.nextLinkSegments?.length, 1); + strictEqual(method.pagingMetadata.nextLinkSegments[0], sdkPackage.models[0].properties[1]); const response = method.response; strictEqual(response.kind, "method"); @@ -71,8 +71,8 @@ describe("typespec-client-generator-core: paged operation", () => { strictEqual(method.name, "test"); strictEqual(method.kind, "paging"); strictEqual(method.nextLinkPath, "next"); - strictEqual(method.nextLinkSegments?.length, 1); - strictEqual(method.nextLinkSegments[0], sdkPackage.models[0].properties[1]); + strictEqual(method.pagingMetadata.nextLinkSegments?.length, 1); + strictEqual(method.pagingMetadata.nextLinkSegments[0], sdkPackage.models[0].properties[1]); const response = method.response; strictEqual(response.kind, "method"); @@ -100,8 +100,8 @@ describe("typespec-client-generator-core: paged operation", () => { strictEqual(method.name, "test"); strictEqual(method.kind, "paging"); strictEqual(method.nextLinkPath, "next"); - strictEqual(method.nextLinkSegments?.length, 1); - strictEqual(method.nextLinkSegments[0], sdkPackage.models[0].properties[1]); + strictEqual(method.pagingMetadata.nextLinkSegments?.length, 1); + strictEqual(method.pagingMetadata.nextLinkSegments[0], sdkPackage.models[0].properties[1]); const response = method.response; strictEqual(response.kind, "method"); @@ -131,8 +131,8 @@ describe("typespec-client-generator-core: paged operation", () => { strictEqual(method.name, "test"); strictEqual(method.kind, "paging"); strictEqual(method.nextLinkPath, "nextLink"); - strictEqual(method.nextLinkSegments?.length, 1); - strictEqual(method.nextLinkSegments[0], sdkPackage.models[0].properties[1]); + strictEqual(method.pagingMetadata.nextLinkSegments?.length, 1); + strictEqual(method.pagingMetadata.nextLinkSegments[0], sdkPackage.models[0].properties[1]); const response = method.response; strictEqual(response.kind, "method"); @@ -165,10 +165,13 @@ describe("typespec-client-generator-core: paged operation", () => { strictEqual(method.name, "test"); strictEqual(method.kind, "paging"); strictEqual(method.nextLinkPath, "pagination.nextLink"); - strictEqual(method.nextLinkSegments?.length, 2); - strictEqual(method.nextLinkSegments[0], sdkPackage.models[0].properties[1]); + 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.nextLinkSegments[1], sdkPackage.models[0].properties[1].type.properties[0]); + strictEqual( + method.pagingMetadata.nextLinkSegments[1], + sdkPackage.models[0].properties[1].type.properties[0], + ); const response = method.response; strictEqual(response.kind, "method"); @@ -231,8 +234,8 @@ describe("typespec-client-generator-core: paged operation", () => { strictEqual(method.name, "test"); strictEqual(method.kind, "paging"); strictEqual(method.nextLinkPath, "nextLink"); - strictEqual(method.nextLinkSegments?.length, 1); - strictEqual(method.nextLinkSegments[0], sdkPackage.models[1].properties[1]); + strictEqual(method.pagingMetadata.nextLinkSegments?.length, 1); + strictEqual(method.pagingMetadata.nextLinkSegments[0], sdkPackage.models[1].properties[1]); const response = method.response; strictEqual(response.kind, "method"); @@ -260,16 +263,19 @@ describe("typespec-client-generator-core: paged operation", () => { const method = getServiceMethodOfClient(sdkPackage); strictEqual(method.name, "test"); strictEqual(method.kind, "paging"); - strictEqual(method.continuationTokenParameterSegments?.length, 1); - strictEqual(method.continuationTokenParameterSegments?.[0], method.parameters[0]); + 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.continuationTokenResponseSegments?.length, 1); + strictEqual(method.pagingMetadata.continuationTokenResponseSegments?.length, 1); strictEqual(method.operation.responses[0].type?.kind, "model"); strictEqual( - method.continuationTokenResponseSegments?.[0], + method.pagingMetadata.continuationTokenResponseSegments?.[0], method.operation.responses[0].type.properties[1], ); }); @@ -292,12 +298,15 @@ describe("typespec-client-generator-core: paged operation", () => { const method = getServiceMethodOfClient(sdkPackage); strictEqual(method.name, "test"); strictEqual(method.kind, "paging"); - strictEqual(method.continuationTokenParameterSegments?.length, 1); - strictEqual(method.continuationTokenParameterSegments?.[0], method.parameters[0]); - strictEqual(method.continuationTokenResponseSegments?.length, 1); + 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.continuationTokenResponseSegments?.[0], + method.pagingMetadata.continuationTokenResponseSegments?.[0], method.operation.responses[0].type.properties[1], ); }); @@ -320,12 +329,15 @@ describe("typespec-client-generator-core: paged operation", () => { const method = getServiceMethodOfClient(sdkPackage); strictEqual(method.name, "test"); strictEqual(method.kind, "paging"); - strictEqual(method.continuationTokenParameterSegments?.length, 1); - strictEqual(method.continuationTokenParameterSegments?.[0], method.parameters[0]); - strictEqual(method.continuationTokenResponseSegments?.length, 1); + 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.continuationTokenResponseSegments?.[0], + method.pagingMetadata.continuationTokenResponseSegments?.[0], method.operation.responses[0].type.properties[1], ); }); @@ -349,12 +361,15 @@ describe("typespec-client-generator-core: paged operation", () => { const method = getServiceMethodOfClient(sdkPackage); strictEqual(method.name, "test"); strictEqual(method.kind, "paging"); - strictEqual(method.continuationTokenParameterSegments?.length, 1); - strictEqual(method.continuationTokenParameterSegments?.[0], method.parameters[0]); - strictEqual(method.continuationTokenResponseSegments?.length, 1); + 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.continuationTokenResponseSegments?.[0], + method.pagingMetadata.continuationTokenResponseSegments?.[0], method.operation.responses[0].headers[0], ); }); @@ -378,12 +393,15 @@ describe("typespec-client-generator-core: paged operation", () => { const method = getServiceMethodOfClient(sdkPackage); strictEqual(method.name, "test"); strictEqual(method.kind, "paging"); - strictEqual(method.continuationTokenParameterSegments?.length, 1); - strictEqual(method.continuationTokenParameterSegments?.[0], method.parameters[0]); - strictEqual(method.continuationTokenResponseSegments?.length, 1); + 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.continuationTokenResponseSegments?.[0], + method.pagingMetadata.continuationTokenResponseSegments?.[0], method.operation.responses[0].headers[0], ); }); @@ -407,12 +425,15 @@ describe("typespec-client-generator-core: paged operation", () => { const method = getServiceMethodOfClient(sdkPackage); strictEqual(method.name, "test"); strictEqual(method.kind, "paging"); - strictEqual(method.continuationTokenParameterSegments?.length, 1); - strictEqual(method.continuationTokenParameterSegments?.[0], method.parameters[0]); - strictEqual(method.continuationTokenResponseSegments?.length, 1); + 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.continuationTokenResponseSegments?.[0], + method.pagingMetadata.continuationTokenResponseSegments?.[0], method.operation.responses[0].headers[0], ); }); @@ -446,21 +467,24 @@ describe("typespec-client-generator-core: paged operation", () => { const method = getServiceMethodOfClient(sdkPackage); strictEqual(method.name, "test"); strictEqual(method.kind, "paging"); - strictEqual(method.continuationTokenParameterSegments?.length, 2); - strictEqual(method.continuationTokenParameterSegments?.[0], method.parameters[0]); + strictEqual(method.pagingMetadata.continuationTokenParameterSegments?.length, 2); + strictEqual( + method.pagingMetadata.continuationTokenParameterSegments?.[0], + method.parameters[0], + ); strictEqual(method.parameters[0].type.kind, "model"); strictEqual( - method.continuationTokenParameterSegments?.[1], + 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.continuationTokenResponseSegments?.length, 1); + strictEqual(method.pagingMetadata.continuationTokenResponseSegments?.length, 1); strictEqual(method.operation.responses[0].type?.kind, "model"); strictEqual( - method.continuationTokenResponseSegments?.[0], + method.pagingMetadata.continuationTokenResponseSegments?.[0], method.operation.responses[0].type.properties[1], ); }); From a4a1f928aa8c4bbd02f9baa02b5eb73f5526c337 Mon Sep 17 00:00:00 2001 From: tadelesh Date: Tue, 18 Feb 2025 16:52:21 +0800 Subject: [PATCH 12/12] fix --- .chronus/changes/continuation_token-2025-1-14-16-15-29.md | 2 +- .chronus/changes/continuation_token-2025-1-14-16-17-22.md | 2 +- .chronus/changes/continuation_token-2025-1-18-16-47-36.md | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) 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 index 43959e6d8a..3c3d33d7f7 100644 --- a/.chronus/changes/continuation_token-2025-1-14-16-15-29.md +++ b/.chronus/changes/continuation_token-2025-1-14-16-15-29.md @@ -4,4 +4,4 @@ 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 +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 index 0b67b17a23..6de7262633 100644 --- a/.chronus/changes/continuation_token-2025-1-14-16-17-22.md +++ b/.chronus/changes/continuation_token-2025-1-14-16-17-22.md @@ -4,4 +4,4 @@ packages: - "@azure-tools/typespec-client-generator-core" --- -Deprecate `resultPath` in `SdkMethodResponse`. Use `resultSegments` instead. \ No newline at end of file +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 index 962d9a6a54..2ac3360fe1 100644 --- a/.chronus/changes/continuation_token-2025-1-18-16-47-36.md +++ b/.chronus/changes/continuation_token-2025-1-18-16-47-36.md @@ -4,4 +4,4 @@ packages: - "@azure-tools/typespec-client-generator-core" --- -Add `SdkPagingServiceMetadata` type to store all paging related info. \ No newline at end of file +Add `SdkPagingServiceMetadata` type to store all paging related info. \ No newline at end of file