diff --git a/.chronus/changes/typespec-go-relative-nextlink-2026-07-28-15-45-00.md b/.chronus/changes/typespec-go-relative-nextlink-2026-07-28-15-45-00.md new file mode 100644 index 0000000000..977eafd6d5 --- /dev/null +++ b/.chronus/changes/typespec-go-relative-nextlink-2026-07-28-15-45-00.md @@ -0,0 +1,7 @@ +--- +changeKind: feature +packages: + - "@azure-tools/typespec-go" +--- + +Support paging with a relative nextLink. Pagers now pass the client endpoint to `runtime.FetcherForNextLink` so a next link relative to the endpoint can be resolved; absolute next links are unchanged. Requires azcore v1.22.1 or later. diff --git a/packages/typespec-go/src/codegen/core/operations.ts b/packages/typespec-go/src/codegen/core/operations.ts index 19631b039f..4ecd18ed09 100644 --- a/packages/typespec-go/src/codegen/core/operations.ts +++ b/packages/typespec-go/src/codegen/core/operations.ts @@ -179,9 +179,9 @@ export function generateOperations( // it must be done before the imports are written out if (go.isLROMethod(method)) { // generate Begin method - opText += generateLROBeginMethod(method, options, imports, indent); + opText += generateLROBeginMethod(method, options, imports, indent, azureARM); } - opText += generateOperation(method, options, imports, indent); + opText += generateOperation(method, options, imports, indent, azureARM); opText += createProtocolRequest(azureARM, method, imports, indent); if (method.kind !== "lroMethod") { // LRO responses are handled elsewhere, with the exception of pageable LROs @@ -691,6 +691,31 @@ function generateNilChecks( return checks.join(" && "); } +/** + * returns the expression used to obtain the client's service endpoint, or undefined + * when the endpoint can't be determined from the client alone. + * + * @param client the client for which to obtain the endpoint expression + * @param azureARM indicates if the client is an ARM client + * @returns the endpoint expression or undefined + */ +function getClientEndpointExpr(client: go.Client, azureARM: boolean): string | undefined { + if (azureARM) { + // for ARM, the endpoint is handled via the azcore/arm.Client + return "client.internal.Endpoint()"; + } + if (client.instance?.kind === "templatedHost") { + // NOTE: this is an Autorest-only compat case. the host is assembled per + // method as it can require method params, so there's no client endpoint. + return undefined; + } + const hostParams = client.parameters.filter((param) => param.kind === "uriParam"); + if (hostParams.length === 1) { + return `client.${hostParams[0].name}`; + } + return undefined; +} + /** * emits code that calls runtime.NewPager * @@ -698,6 +723,7 @@ function generateNilChecks( * @param options emitter options * @param imports the import manager currently in scope * @param indent the indentation helper currently in scope + * @param azureARM indicates if the client is an ARM client * @returns the complete call to runtime.NewPager(...) */ function emitPagerDefinition( @@ -705,6 +731,7 @@ function emitPagerDefinition( options: go.Options, imports: ImportManager, indent: helpers.Indentation, + azureARM: boolean, ): string { imports.add("context"); let text = `runtime.NewPager(runtime.PagingHandler[${method.returns.name}]{\n`; @@ -806,6 +833,11 @@ function emitPagerDefinition( } case "nextLink": { const nextLinkPath = helpers.buildNextLinkPath(method.strategy); + // a custom next page operation builds the request itself, so the next + // link must be passed through to it unmodified. + const endpointExpr = method.strategy.method + ? undefined + : getClientEndpointExpr(method.receiver.type, azureARM); let nextLinkVar: string; if (method.kind === "pageableMethod") { text += `${indent.get()}nextLink := ""\n`; @@ -820,30 +852,37 @@ function emitPagerDefinition( text += `${indent.push().get()}return client.${method.naming.requestMethod}(${reqParams})\n`; text += `${indent.pop().get()}}, `; // nextPageMethod might be absent in some cases, see https://github.com/Azure/autorest/issues/4393 - if (method.strategy.method) { - const nextOpParams = helpers - .getCreateRequestParametersSig(method.strategy.method) - .split(","); - // keep the parameter names from the name/type tuples and find nextLink param - for (let i = 0; i < nextOpParams.length; ++i) { - const paramName = nextOpParams[i].trim().split(" ")[0]; - const paramType = nextOpParams[i].trim().split(" ")[1]; - if (paramName.startsWith("next") && paramType === "string") { - nextOpParams[i] = "encodedNextLink"; - } else { - nextOpParams[i] = paramName; + const nextReq = method.strategy.method; + const httpVerb = + !nextReq && method.nextLinkVerb !== "get" + ? `http.Method${naming.capitalize(method.nextLinkVerb)}` + : undefined; + if (nextReq || httpVerb || endpointExpr) { + text += `&runtime.FetcherForNextLinkOptions{\n`; + indent.push(); + if (nextReq) { + const nextOpParams = helpers.getCreateRequestParametersSig(nextReq).split(","); + // keep the parameter names from the name/type tuples and find nextLink param + for (let i = 0; i < nextOpParams.length; ++i) { + const paramName = nextOpParams[i].trim().split(" ")[0]; + const paramType = nextOpParams[i].trim().split(" ")[1]; + if (paramName.startsWith("next") && paramType === "string") { + nextOpParams[i] = "encodedNextLink"; + } else { + nextOpParams[i] = paramName; + } } + // add a definition for the nextReq func that uses the nextLinkOperation + text += `${indent.get()}NextReq: func(ctx context.Context, encodedNextLink string) (*policy.Request, error) {\n`; + text += `${indent.push().get()}return client.${nextReq.name}(${nextOpParams.join(", ")})\n`; + text += `${indent.pop().get()}},\n`; + } + if (httpVerb) { + text += `${indent.get()}HTTPVerb: ${httpVerb},\n`; + } + if (endpointExpr) { + text += `${indent.get()}Endpoint: ${endpointExpr},\n`; } - // add a definition for the nextReq func that uses the nextLinkOperation - indent.push(); - text += `&runtime.FetcherForNextLinkOptions{\n`; - text += `${indent.get()}NextReq: func(ctx context.Context, encodedNextLink string) (*policy.Request, error) {\n`; - text += `${indent.push().get()}return client.${method.strategy.method.name}(${nextOpParams.join(", ")})\n`; - text += `${indent.pop().get()}},\n`; - text += `${indent.pop().get()}})\n`; - } else if (method.nextLinkVerb !== "get") { - text += `&runtime.FetcherForNextLinkOptions{\n`; - text += `${indent.push().get()}HTTPVerb: http.Method${naming.capitalize(method.nextLinkVerb)},\n`; text += `${indent.pop().get()}})\n`; } else { text += "nil)\n"; @@ -937,6 +976,7 @@ function generateOperation( options: go.Options, imports: ImportManager, indent: helpers.Indentation, + azureARM: boolean, ): string { const params = getAPIParametersSig(method, imports); const returns = generateReturnsInfo(method, "op"); @@ -965,7 +1005,7 @@ function generateOperation( text += `func ${getClientReceiverDefinition(method.receiver)} ${methodName}(${params}) (${returns.join(", ")}) {\n`; if (method.kind === "pageableMethod") { text += `${indent.get()}return `; - text += emitPagerDefinition(method, options, imports, indent); + text += emitPagerDefinition(method, options, imports, indent, azureARM); text += "}\n\n"; return text; } @@ -2122,6 +2162,7 @@ function generateLROBeginMethod( options: go.Options, imports: ImportManager, indent: helpers.Indentation, + azureARM: boolean, ): string { const params = getAPIParametersSig(method, imports); const returns = generateReturnsInfo(method, "api"); @@ -2144,7 +2185,7 @@ function generateLROBeginMethod( pollerTypeParam = `[*runtime.Pager${pollerTypeParam}]`; pollerType = "&pager"; text += `${indent.get()}pager := `; - text += emitPagerDefinition(method, options, imports, indent); + text += emitPagerDefinition(method, options, imports, indent, azureARM); } text += `${indent.get()}if options == nil || options.ResumeToken == "" {\n`; diff --git a/packages/typespec-go/test/azure-http-specs/azure/core/azurepagegroup/custom_client_test.go b/packages/typespec-go/test/azure-http-specs/azure/core/azurepagegroup/custom_client_test.go index 9b8dfac4fe..9567324009 100644 --- a/packages/typespec-go/test/azure-http-specs/azure/core/azurepagegroup/custom_client_test.go +++ b/packages/typespec-go/test/azure-http-specs/azure/core/azurepagegroup/custom_client_test.go @@ -110,8 +110,7 @@ func TestPageClient_NewListWithParametersPager(t *testing.T) { require.EqualValues(t, 2, pages) }*/ -// TODO: runtime.FetcherForNextLink doesn't support relative next link URLs -/*func TestPageClient_NewWithRelativeNextLinkPager(t *testing.T) { +func TestPageClient_NewWithRelativeNextLinkPager(t *testing.T) { client, err := azurepagegroup.NewPageClientWithNoCredential("http://localhost:3000", nil) require.NoError(t, err) pager := client.NewWithRelativeNextLinkPager(nil) @@ -142,4 +141,4 @@ func TestPageClient_NewListWithParametersPager(t *testing.T) { } } require.EqualValues(t, 2, pages) -}*/ +} diff --git a/packages/typespec-go/test/unittest/scenarios/pageable-lro.md b/packages/typespec-go/test/unittest/scenarios/pageable-lro.md index 7ce3448bf1..b2675b9306 100644 --- a/packages/typespec-go/test/unittest/scenarios/pageable-lro.md +++ b/packages/typespec-go/test/unittest/scenarios/pageable-lro.md @@ -104,7 +104,9 @@ func (client *PageableLROsClient) BeginListPrivateEndPoints(ctx context.Context, Fetcher: func(ctx context.Context, page *PageableLROsClientListPrivateEndPointsResponse) (PageableLROsClientListPrivateEndPointsResponse, error) { resp, err := runtime.FetcherForNextLink(ctx, client.internal.Pipeline(), *page.NextLink, func(ctx context.Context) (*policy.Request, error) { return client.listPrivateEndPointsCreateRequest(ctx, apiVersion, resourceGroupName, resourceName, options) - }, nil) + }, &runtime.FetcherForNextLinkOptions{ + Endpoint: client.internal.Endpoint(), + }) if err != nil { return PageableLROsClientListPrivateEndPointsResponse{}, err } diff --git a/packages/typespec-go/test/unittest/scenarios/tenant-resource.md b/packages/typespec-go/test/unittest/scenarios/tenant-resource.md index 502a1f2e65..46bf30bbf7 100644 --- a/packages/typespec-go/test/unittest/scenarios/tenant-resource.md +++ b/packages/typespec-go/test/unittest/scenarios/tenant-resource.md @@ -289,7 +289,9 @@ func (client *TenantItemsClient) NewListPager(apiVersion string, options *Tenant } resp, err := runtime.FetcherForNextLink(ctx, client.internal.Pipeline(), nextLink, func(ctx context.Context) (*policy.Request, error) { return client.listCreateRequest(ctx, apiVersion, options) - }, nil) + }, &runtime.FetcherForNextLinkOptions{ + Endpoint: client.internal.Endpoint(), + }) if err != nil { return TenantItemsClientListResponse{}, err }