From 92e010d8b8be1b32a26286336dd4e48e40a6c018 Mon Sep 17 00:00:00 2001 From: tadelesh Date: Tue, 4 Aug 2026 15:26:03 +0800 Subject: [PATCH 1/6] fix(typespec-go): honor path API version overrides Store defaulted path API versions on generated clients so ClientOptions.APIVersion is applied while constructing request paths. Propagate the value and query/header policy metadata across sub-client hierarchies. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: a2fed377-7b26-45fc-b3aa-8b6d6df8b46a --- ...pi-version-override-2026-08-04-14-45-00.md | 7 + .../src/codegen/core/operations.ts | 14 +- .../typespec-go/src/tcgcadapter/clients.ts | 79 +++++-- .../versionedgroup/versioned_client_test.go | 32 +++ .../path-api-version-override.test.ts | 5 + .../subclient-api-version-policy.test.ts | 7 + ...ubclient-path-api-version-override.test.ts | 7 + .../scenarios/path-api-version-override.md | 157 ++++++++++++++ .../scenarios/subclient-api-version-policy.md | 196 +++++++++++++++++ .../subclient-path-api-version-override.md | 198 ++++++++++++++++++ 10 files changed, 687 insertions(+), 15 deletions(-) create mode 100644 .chronus/changes/fix-go-path-api-version-override-2026-08-04-14-45-00.md create mode 100644 packages/typespec-go/test/unittest/scenario-suites/path-api-version-override.test.ts create mode 100644 packages/typespec-go/test/unittest/scenario-suites/subclient-api-version-policy.test.ts create mode 100644 packages/typespec-go/test/unittest/scenario-suites/subclient-path-api-version-override.test.ts create mode 100644 packages/typespec-go/test/unittest/scenarios/path-api-version-override.md create mode 100644 packages/typespec-go/test/unittest/scenarios/subclient-api-version-policy.md create mode 100644 packages/typespec-go/test/unittest/scenarios/subclient-path-api-version-override.md diff --git a/.chronus/changes/fix-go-path-api-version-override-2026-08-04-14-45-00.md b/.chronus/changes/fix-go-path-api-version-override-2026-08-04-14-45-00.md new file mode 100644 index 0000000000..7aa5667bd6 --- /dev/null +++ b/.chronus/changes/fix-go-path-api-version-override-2026-08-04-14-45-00.md @@ -0,0 +1,7 @@ +--- +changeKind: fix +packages: + - "@azure-tools/typespec-go" +--- + +Honor `ClientOptions.APIVersion` for API versions emitted in operation paths, including sub-client operations. diff --git a/packages/typespec-go/src/codegen/core/operations.ts b/packages/typespec-go/src/codegen/core/operations.ts index 5ef8929151..7a759f43c3 100644 --- a/packages/typespec-go/src/codegen/core/operations.ts +++ b/packages/typespec-go/src/codegen/core/operations.ts @@ -158,7 +158,11 @@ export function generateOperations( for (const param of client.parameters) { if (go.isLiteralParameter(param.style)) { continue; - } else if (clientAccessor.returns.parameters.some((p) => p.name === param.name)) { + } else if ( + clientAccessor.returns.parameters.some( + (p) => p.name === param.name && !go.isLiteralParameter(p.style), + ) + ) { // only propagate ctor params that are common between parent/child initFields.push(`${param.name}: client.${param.name}`); } @@ -312,7 +316,13 @@ function generateConstructors( case "queryScalarParam": case "uriParam": if (param.isApiVersion) { - apiVersionParam = param; + const currentIsPath = + apiVersionParam?.kind === "pathScalarParam" || + apiVersionParam?.kind === "uriParam"; + const paramIsPath = param.kind === "pathScalarParam" || param.kind === "uriParam"; + if (!apiVersionParam || (currentIsPath && !paramIsPath)) { + apiVersionParam = param; + } } } } diff --git a/packages/typespec-go/src/tcgcadapter/clients.ts b/packages/typespec-go/src/tcgcadapter/clients.ts index bc239aa0e2..1e4a4915fa 100644 --- a/packages/typespec-go/src/tcgcadapter/clients.ts +++ b/packages/typespec-go/src/tcgcadapter/clients.ts @@ -1251,12 +1251,29 @@ export class ClientAdapter { // we must check via param name and not reference equality. this is because a client param // can be used in multiple ways. e.g. a client param "apiVersion" that's used as a path param - // in one method and a query param in another. - if ( - !method.receiver.type.parameters.find((v: go.ClientParameter) => { - return v.name === adaptedParam.name; - }) - ) { + // in one method and a query param in another. path API versions are kept separately because + // they require a client field while query/header API versions are handled by the pipeline. + const isPathAPIVersionWithDefault = + adaptedParam.kind === "pathScalarParam" && + adaptedParam.isApiVersion && + go.isClientSideDefault(adaptedParam.style); + const addClientParameter = (client: go.Client): void => { + const existingParam = client.parameters.find((v: go.ClientParameter) => { + if (v.name !== adaptedParam.name) { + return false; + } + if (!go.isAPIVersionParameter(v) || !go.isAPIVersionParameter(adaptedParam)) { + return true; + } + return ( + v.kind === adaptedParam.kind || + (v.kind !== "pathScalarParam" && adaptedParam.kind !== "pathScalarParam") + ); + }); + if (existingParam) { + return; + } + if ( this.ta.codeModel.type === "azure-arm" && adaptedParam.style !== "literal" && @@ -1268,13 +1285,30 @@ export class ClientAdapter { opParam.__raw?.node, ); } - method.receiver.type.parameters.push(adaptedParam); - if (method.receiver.type.instance?.kind === "constructable") { + + client.parameters.push(adaptedParam); + if (client.instance?.kind === "constructable") { // if this is an instantiable client then also add // the client parameter to all constructors - for (const ctor of method.receiver.type.instance.constructors) { + for (const ctor of client.instance.constructors) { ctor.parameters.push(adaptedParam); } + if ( + client.instance.options.kind === "clientOptions" && + isPathAPIVersionWithDefault && + !client.instance.options.parameters.some((param) => param.name === adaptedParam.name) + ) { + client.instance.options.parameters.push(adaptedParam); + } + } + }; + + addClientParameter(method.receiver.type); + if (go.isAPIVersionParameter(adaptedParam)) { + let parent = method.receiver.type.parent; + while (parent) { + addClientParameter(parent); + parent = parent.parent; } } } @@ -1317,8 +1351,9 @@ export class ClientAdapter { | tcgc.SdkQueryParameter, ): go.MethodParameter { if (opParam.isApiVersionParam) { - // we emit the api version param inline as a literal, never as a param. - // the ClientOptions.APIVersion setting is used to change the version. + // Header/query API versions are emitted inline and overridden by the pipeline. + // Path API versions must be stored on the client because the pipeline cannot + // replace a path segment after the request URL has been constructed. let paramType: go.Literal | go.String; let paramStyle: go.ParameterStyle; if (opParam.clientDefaultValue) { @@ -1335,8 +1370,23 @@ export class ClientAdapter { ); client.apiVersions.push(versionConst); } - paramType = new go.Literal(versionConst, versionConst.name); - paramStyle = "literal"; + const versionLiteral = new go.Literal(versionConst, versionConst.name); + let rootClient = client; + while (rootClient.parent) { + rootClient = rootClient.parent; + } + if ( + opParam.kind === "path" && + opParam.onClient && + rootClient.instance?.kind === "constructable" && + rootClient.instance.options.kind === "clientOptions" + ) { + paramType = this.ta.getStringType(); + paramStyle = new go.ClientSideDefault(versionLiteral); + } else { + paramType = versionLiteral; + paramStyle = "literal"; + } } else { paramType = this.ta.getStringType(); paramStyle = opParam.optional ? "optional" : "required"; @@ -1366,6 +1416,9 @@ export class ClientAdapter { true, paramLoc, ); + if (go.isClientSideDefault(paramStyle)) { + apiVersionParam.omitEmptyStringCheck = true; + } break; case "query": apiVersionParam = new go.QueryScalarParameter( diff --git a/packages/typespec-go/test/http-specs/server/versions/versionedgroup/versioned_client_test.go b/packages/typespec-go/test/http-specs/server/versions/versionedgroup/versioned_client_test.go index 63376c7f34..55524a8b56 100644 --- a/packages/typespec-go/test/http-specs/server/versions/versionedgroup/versioned_client_test.go +++ b/packages/typespec-go/test/http-specs/server/versions/versionedgroup/versioned_client_test.go @@ -5,6 +5,7 @@ package versionedgroup_test import ( "context" + "net/http" "testing" "versionedgroup" @@ -12,6 +13,20 @@ import ( "github.com/stretchr/testify/require" ) +type captureTransport struct { + request *http.Request +} + +func (c *captureTransport) Do(request *http.Request) (*http.Response, error) { + c.request = request + return &http.Response{ + StatusCode: http.StatusOK, + Header: http.Header{}, + Body: http.NoBody, + Request: request, + }, nil +} + func TestVersionedClient_WithPathAPIVersion(t *testing.T) { client, err := versionedgroup.NewVersionedClientWithNoCredential("http://localhost:3000", nil) require.NoError(t, err) @@ -20,6 +35,23 @@ func TestVersionedClient_WithPathAPIVersion(t *testing.T) { require.True(t, resp.Success) } +func TestVersionedClient_WithPathAPIVersionOverride(t *testing.T) { + const apiVersion = "2023-01-01-preview" + transport := &captureTransport{} + client, err := versionedgroup.NewVersionedClientWithNoCredential("http://localhost:3000", &versionedgroup.VersionedClientOptions{ + ClientOptions: azcore.ClientOptions{ + APIVersion: apiVersion, + Transport: transport, + }, + }) + require.NoError(t, err) + + resp, err := client.WithPathAPIVersion(context.Background(), nil) + require.NoError(t, err) + require.True(t, resp.Success) + require.Equal(t, "/server/versions/versioned/with-path-api-version/"+apiVersion, transport.request.URL.Path) +} + func TestVersionedClient_WithQueryAPIVersion(t *testing.T) { client, err := versionedgroup.NewVersionedClientWithNoCredential("http://localhost:3000", nil) require.NoError(t, err) diff --git a/packages/typespec-go/test/unittest/scenario-suites/path-api-version-override.test.ts b/packages/typespec-go/test/unittest/scenario-suites/path-api-version-override.test.ts new file mode 100644 index 0000000000..52e3bdb29c --- /dev/null +++ b/packages/typespec-go/test/unittest/scenario-suites/path-api-version-override.test.ts @@ -0,0 +1,5 @@ +// Generated by `pnpm gen:scenario-suites`. Do not edit by hand. +import { resolvePath } from "@typespec/compiler"; +import { describeScenarioFile } from "../scenario-runner.js"; + +describeScenarioFile(resolvePath(import.meta.dirname, "../scenarios/path-api-version-override.md")); diff --git a/packages/typespec-go/test/unittest/scenario-suites/subclient-api-version-policy.test.ts b/packages/typespec-go/test/unittest/scenario-suites/subclient-api-version-policy.test.ts new file mode 100644 index 0000000000..703329259e --- /dev/null +++ b/packages/typespec-go/test/unittest/scenario-suites/subclient-api-version-policy.test.ts @@ -0,0 +1,7 @@ +// Generated by `pnpm gen:scenario-suites`. Do not edit by hand. +import { resolvePath } from "@typespec/compiler"; +import { describeScenarioFile } from "../scenario-runner.js"; + +describeScenarioFile( + resolvePath(import.meta.dirname, "../scenarios/subclient-api-version-policy.md"), +); diff --git a/packages/typespec-go/test/unittest/scenario-suites/subclient-path-api-version-override.test.ts b/packages/typespec-go/test/unittest/scenario-suites/subclient-path-api-version-override.test.ts new file mode 100644 index 0000000000..7d293e2fe4 --- /dev/null +++ b/packages/typespec-go/test/unittest/scenario-suites/subclient-path-api-version-override.test.ts @@ -0,0 +1,7 @@ +// Generated by `pnpm gen:scenario-suites`. Do not edit by hand. +import { resolvePath } from "@typespec/compiler"; +import { describeScenarioFile } from "../scenario-runner.js"; + +describeScenarioFile( + resolvePath(import.meta.dirname, "../scenarios/subclient-path-api-version-override.md"), +); diff --git a/packages/typespec-go/test/unittest/scenarios/path-api-version-override.md b/packages/typespec-go/test/unittest/scenarios/path-api-version-override.md new file mode 100644 index 0000000000..6da3d1c9b2 --- /dev/null +++ b/packages/typespec-go/test/unittest/scenarios/path-api-version-override.md @@ -0,0 +1,157 @@ +# A path API version uses the client-level API version override + +## TypeSpec + +```tsp +@service +@versioned(Versions) +@server( + "{endpoint}", + "Test endpoint", + { + endpoint: url, + } +) +namespace Versioned; + +enum Versions { + v2022_12_01_preview: "2022-12-01-preview", +} + +@head +@route("/with-query-api-version") +op withQueryApiVersion(@query("api-version") apiVersion: string): void; + +@head +@route("/with-path-api-version/{apiVersion}") +op withPathApiVersion(@path apiVersion: string): void; +``` + +## The generated client stores and uses the configured API version for path parameters + +```go client +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. +// Code generated by Microsoft (R) Go Code Generator. DO NOT EDIT. + +package testmodule + +import ( + "context" + "github.com/Azure/azure-sdk-for-go/sdk/azcore" + "github.com/Azure/azure-sdk-for-go/sdk/azcore/policy" + "github.com/Azure/azure-sdk-for-go/sdk/azcore/runtime" + "net/http" + "net/url" + "strings" +) + +// VersionedClient contains the methods for the Versioned group. +// Don't use this type directly, use NewVersionedClientWithNoCredential() instead. +// +// Generated from API version 2022-12-01-preview +type VersionedClient struct { + internal *azcore.Client + apiVersion string + endpoint string +} + +// VersionedClientOptions contains the optional values for creating a [VersionedClient]. +type VersionedClientOptions struct { + azcore.ClientOptions +} + +// NewVersionedClientWithNoCredential creates a new instance of VersionedClient with the specified values. +// - endpoint - Service host +// - options - Contains optional client configuration. Pass nil to accept the default values. +func NewVersionedClientWithNoCredential(endpoint string, options *VersionedClientOptions) (*VersionedClient, error) { + if options == nil { + options = &VersionedClientOptions{} + } + cl, err := azcore.NewClient(moduleName, moduleVersion, runtime.PipelineOptions{ + APIVersion: runtime.APIVersionOptions{ + Name: "api-version", + Location: runtime.APIVersionLocationQueryParam, + }, + }, &options.ClientOptions) + if err != nil { + return nil, err + } + apiVersion := version20221201Preview + if options.APIVersion != "" { + apiVersion = options.APIVersion + } + client := &VersionedClient{ + apiVersion: apiVersion, + endpoint: endpoint, + internal: cl, + } + return client, nil +} + +// WithPathAPIVersion - +// If the operation fails it returns an *azcore.ResponseError type. +// - options - VersionedClientWithPathAPIVersionOptions contains the optional parameters for the VersionedClient.WithPathAPIVersion +// method. +func (client *VersionedClient) WithPathAPIVersion(ctx context.Context, options *VersionedClientWithPathAPIVersionOptions) (VersionedClientWithPathAPIVersionResponse, error) { + var err error + req, err := client.withPathAPIVersionCreateRequest(ctx, options) + if err != nil { + return VersionedClientWithPathAPIVersionResponse{}, err + } + httpResp, err := client.internal.Pipeline().Do(req) + if err != nil { + return VersionedClientWithPathAPIVersionResponse{}, err + } + if !runtime.HasStatusCode(httpResp, http.StatusNoContent) { + err = runtime.NewResponseError(httpResp) + return VersionedClientWithPathAPIVersionResponse{}, err + } + return VersionedClientWithPathAPIVersionResponse{}, nil +} + +// withPathAPIVersionCreateRequest creates the WithPathAPIVersion request. +func (client *VersionedClient) withPathAPIVersionCreateRequest(ctx context.Context, _ *VersionedClientWithPathAPIVersionOptions) (*policy.Request, error) { + urlPath := "/with-path-api-version/{apiVersion}" + urlPath = strings.ReplaceAll(urlPath, "{apiVersion}", url.PathEscape(client.apiVersion)) + req, err := runtime.NewRequest(ctx, http.MethodHead, runtime.JoinPaths(client.endpoint, urlPath)) + if err != nil { + return nil, err + } + return req, nil +} + +// WithQueryAPIVersion - +// If the operation fails it returns an *azcore.ResponseError type. +// - options - VersionedClientWithQueryAPIVersionOptions contains the optional parameters for the VersionedClient.WithQueryAPIVersion +// method. +func (client *VersionedClient) WithQueryAPIVersion(ctx context.Context, options *VersionedClientWithQueryAPIVersionOptions) (VersionedClientWithQueryAPIVersionResponse, error) { + var err error + req, err := client.withQueryAPIVersionCreateRequest(ctx, options) + if err != nil { + return VersionedClientWithQueryAPIVersionResponse{}, err + } + httpResp, err := client.internal.Pipeline().Do(req) + if err != nil { + return VersionedClientWithQueryAPIVersionResponse{}, err + } + if !runtime.HasStatusCode(httpResp, http.StatusNoContent) { + err = runtime.NewResponseError(httpResp) + return VersionedClientWithQueryAPIVersionResponse{}, err + } + return VersionedClientWithQueryAPIVersionResponse{}, nil +} + +// withQueryAPIVersionCreateRequest creates the WithQueryAPIVersion request. +func (client *VersionedClient) withQueryAPIVersionCreateRequest(ctx context.Context, _ *VersionedClientWithQueryAPIVersionOptions) (*policy.Request, error) { + urlPath := "/with-query-api-version" + req, err := runtime.NewRequest(ctx, http.MethodHead, runtime.JoinPaths(client.endpoint, urlPath)) + if err != nil { + return nil, err + } + reqQP := req.Raw().URL.Query() + reqQP.Set("api-version", version20221201Preview) + req.Raw().URL.RawQuery = strings.ReplaceAll(reqQP.Encode(), "+", "%20") + return req, nil +} +``` diff --git a/packages/typespec-go/test/unittest/scenarios/subclient-api-version-policy.md b/packages/typespec-go/test/unittest/scenarios/subclient-api-version-policy.md new file mode 100644 index 0000000000..b3a97e080f --- /dev/null +++ b/packages/typespec-go/test/unittest/scenarios/subclient-api-version-policy.md @@ -0,0 +1,196 @@ +# A sub-client query API version keeps the root client's query policy + +## TypeSpec + +```tsp +@service +@versioned(Versions) +@server( + "{endpoint}", + "Test endpoint", + { + endpoint: url, + } +) +namespace Versioned; + +enum Versions { + v2022_12_01_preview: "2022-12-01-preview", +} + +@head +@route("/with-path-api-version/{apiVersion}") +op withPathApiVersion(@path apiVersion: string): void; + +@route("/sub") +interface SubGroup { + @head + @route("/with-query-api-version") + withQueryApiVersion(@query("api-version") apiVersion: string): void; +} +``` + +## The root client keeps its path field and configures the query policy + +```go versioned_client +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. +// Code generated by Microsoft (R) Go Code Generator. DO NOT EDIT. + +package testmodule + +import ( + "context" + "github.com/Azure/azure-sdk-for-go/sdk/azcore" + "github.com/Azure/azure-sdk-for-go/sdk/azcore/policy" + "github.com/Azure/azure-sdk-for-go/sdk/azcore/runtime" + "net/http" + "net/url" + "strings" +) + +// VersionedClient contains the methods for the Versioned group. +// Don't use this type directly, use NewVersionedClientWithNoCredential() instead. +// +// Generated from API version 2022-12-01-preview +type VersionedClient struct { + internal *azcore.Client + apiVersion string + endpoint string +} + +// VersionedClientOptions contains the optional values for creating a [VersionedClient]. +type VersionedClientOptions struct { + azcore.ClientOptions +} + +// NewVersionedClientWithNoCredential creates a new instance of VersionedClient with the specified values. +// - endpoint - Service host +// - options - Contains optional client configuration. Pass nil to accept the default values. +func NewVersionedClientWithNoCredential(endpoint string, options *VersionedClientOptions) (*VersionedClient, error) { + if options == nil { + options = &VersionedClientOptions{} + } + cl, err := azcore.NewClient(moduleName, moduleVersion, runtime.PipelineOptions{ + APIVersion: runtime.APIVersionOptions{ + Name: "api-version", + Location: runtime.APIVersionLocationQueryParam, + }, + }, &options.ClientOptions) + if err != nil { + return nil, err + } + apiVersion := version20221201Preview + if options.APIVersion != "" { + apiVersion = options.APIVersion + } + client := &VersionedClient{ + apiVersion: apiVersion, + endpoint: endpoint, + internal: cl, + } + return client, nil +} + +// NewVersionedSubGroupClient creates a new instance of [VersionedSubGroupClient]. +func (client *VersionedClient) NewVersionedSubGroupClient() *VersionedSubGroupClient { + return &VersionedSubGroupClient{ + endpoint: client.endpoint, + internal: client.internal, + } +} + +// WithPathAPIVersion - +// If the operation fails it returns an *azcore.ResponseError type. +// - options - VersionedClientWithPathAPIVersionOptions contains the optional parameters for the VersionedClient.WithPathAPIVersion +// method. +func (client *VersionedClient) WithPathAPIVersion(ctx context.Context, options *VersionedClientWithPathAPIVersionOptions) (VersionedClientWithPathAPIVersionResponse, error) { + var err error + req, err := client.withPathAPIVersionCreateRequest(ctx, options) + if err != nil { + return VersionedClientWithPathAPIVersionResponse{}, err + } + httpResp, err := client.internal.Pipeline().Do(req) + if err != nil { + return VersionedClientWithPathAPIVersionResponse{}, err + } + if !runtime.HasStatusCode(httpResp, http.StatusNoContent) { + err = runtime.NewResponseError(httpResp) + return VersionedClientWithPathAPIVersionResponse{}, err + } + return VersionedClientWithPathAPIVersionResponse{}, nil +} + +// withPathAPIVersionCreateRequest creates the WithPathAPIVersion request. +func (client *VersionedClient) withPathAPIVersionCreateRequest(ctx context.Context, _ *VersionedClientWithPathAPIVersionOptions) (*policy.Request, error) { + urlPath := "/with-path-api-version/{apiVersion}" + urlPath = strings.ReplaceAll(urlPath, "{apiVersion}", url.PathEscape(client.apiVersion)) + req, err := runtime.NewRequest(ctx, http.MethodHead, runtime.JoinPaths(client.endpoint, urlPath)) + if err != nil { + return nil, err + } + return req, nil +} +``` + +## The query-only sub-client does not receive a nonexistent API version field + +```go versionedsubgroup_client +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. +// Code generated by Microsoft (R) Go Code Generator. DO NOT EDIT. + +package testmodule + +import ( + "context" + "github.com/Azure/azure-sdk-for-go/sdk/azcore" + "github.com/Azure/azure-sdk-for-go/sdk/azcore/policy" + "github.com/Azure/azure-sdk-for-go/sdk/azcore/runtime" + "net/http" + "strings" +) + +// VersionedSubGroupClient contains the methods for the VersionedSubGroup group. +// Don't use this type directly, use [VersionedClient.NewVersionedSubGroupClient] instead. +// +// Generated from API version 2022-12-01-preview +type VersionedSubGroupClient struct { + internal *azcore.Client + endpoint string +} + +// WithQueryAPIVersion - +// If the operation fails it returns an *azcore.ResponseError type. +// - options - VersionedSubGroupClientWithQueryAPIVersionOptions contains the optional parameters for the VersionedSubGroupClient.WithQueryAPIVersion +// method. +func (client *VersionedSubGroupClient) WithQueryAPIVersion(ctx context.Context, options *VersionedSubGroupClientWithQueryAPIVersionOptions) (VersionedSubGroupClientWithQueryAPIVersionResponse, error) { + var err error + req, err := client.withQueryAPIVersionCreateRequest(ctx, options) + if err != nil { + return VersionedSubGroupClientWithQueryAPIVersionResponse{}, err + } + httpResp, err := client.internal.Pipeline().Do(req) + if err != nil { + return VersionedSubGroupClientWithQueryAPIVersionResponse{}, err + } + if !runtime.HasStatusCode(httpResp, http.StatusNoContent) { + err = runtime.NewResponseError(httpResp) + return VersionedSubGroupClientWithQueryAPIVersionResponse{}, err + } + return VersionedSubGroupClientWithQueryAPIVersionResponse{}, nil +} + +// withQueryAPIVersionCreateRequest creates the WithQueryAPIVersion request. +func (client *VersionedSubGroupClient) withQueryAPIVersionCreateRequest(ctx context.Context, _ *VersionedSubGroupClientWithQueryAPIVersionOptions) (*policy.Request, error) { + urlPath := "/sub/with-query-api-version" + req, err := runtime.NewRequest(ctx, http.MethodHead, runtime.JoinPaths(client.endpoint, urlPath)) + if err != nil { + return nil, err + } + reqQP := req.Raw().URL.Query() + reqQP.Set("api-version", version20221201Preview) + req.Raw().URL.RawQuery = strings.ReplaceAll(reqQP.Encode(), "+", "%20") + return req, nil +} +``` diff --git a/packages/typespec-go/test/unittest/scenarios/subclient-path-api-version-override.md b/packages/typespec-go/test/unittest/scenarios/subclient-path-api-version-override.md new file mode 100644 index 0000000000..cfeb3871ba --- /dev/null +++ b/packages/typespec-go/test/unittest/scenarios/subclient-path-api-version-override.md @@ -0,0 +1,198 @@ +# A sub-client inherits the client-level API version override for path parameters + +## TypeSpec + +```tsp +@service +@versioned(Versions) +@server( + "{endpoint}", + "Test endpoint", + { + endpoint: url, + } +) +namespace Versioned; + +enum Versions { + v2022_12_01_preview: "2022-12-01-preview", +} + +@head +@route("/with-query-api-version") +op withQueryApiVersion(@query("api-version") apiVersion: string): void; + +@route("/sub") +interface SubGroup { + @head + @route("/with-path-api-version/{apiVersion}") + withPathApiVersion(@path apiVersion: string): void; +} +``` + +## The root client stores and propagates the API version + +```go versioned_client +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. +// Code generated by Microsoft (R) Go Code Generator. DO NOT EDIT. + +package testmodule + +import ( + "context" + "github.com/Azure/azure-sdk-for-go/sdk/azcore" + "github.com/Azure/azure-sdk-for-go/sdk/azcore/policy" + "github.com/Azure/azure-sdk-for-go/sdk/azcore/runtime" + "net/http" + "strings" +) + +// VersionedClient contains the methods for the Versioned group. +// Don't use this type directly, use NewVersionedClientWithNoCredential() instead. +// +// Generated from API version 2022-12-01-preview +type VersionedClient struct { + internal *azcore.Client + apiVersion string + endpoint string +} + +// VersionedClientOptions contains the optional values for creating a [VersionedClient]. +type VersionedClientOptions struct { + azcore.ClientOptions +} + +// NewVersionedClientWithNoCredential creates a new instance of VersionedClient with the specified values. +// - endpoint - Service host +// - options - Contains optional client configuration. Pass nil to accept the default values. +func NewVersionedClientWithNoCredential(endpoint string, options *VersionedClientOptions) (*VersionedClient, error) { + if options == nil { + options = &VersionedClientOptions{} + } + cl, err := azcore.NewClient(moduleName, moduleVersion, runtime.PipelineOptions{ + APIVersion: runtime.APIVersionOptions{ + Name: "api-version", + Location: runtime.APIVersionLocationQueryParam, + }, + }, &options.ClientOptions) + if err != nil { + return nil, err + } + apiVersion := version20221201Preview + if options.APIVersion != "" { + apiVersion = options.APIVersion + } + client := &VersionedClient{ + apiVersion: apiVersion, + endpoint: endpoint, + internal: cl, + } + return client, nil +} + +// NewVersionedSubGroupClient creates a new instance of [VersionedSubGroupClient]. +func (client *VersionedClient) NewVersionedSubGroupClient() *VersionedSubGroupClient { + return &VersionedSubGroupClient{ + apiVersion: client.apiVersion, + endpoint: client.endpoint, + internal: client.internal, + } +} + +// WithQueryAPIVersion - +// If the operation fails it returns an *azcore.ResponseError type. +// - options - VersionedClientWithQueryAPIVersionOptions contains the optional parameters for the VersionedClient.WithQueryAPIVersion +// method. +func (client *VersionedClient) WithQueryAPIVersion(ctx context.Context, options *VersionedClientWithQueryAPIVersionOptions) (VersionedClientWithQueryAPIVersionResponse, error) { + var err error + req, err := client.withQueryAPIVersionCreateRequest(ctx, options) + if err != nil { + return VersionedClientWithQueryAPIVersionResponse{}, err + } + httpResp, err := client.internal.Pipeline().Do(req) + if err != nil { + return VersionedClientWithQueryAPIVersionResponse{}, err + } + if !runtime.HasStatusCode(httpResp, http.StatusNoContent) { + err = runtime.NewResponseError(httpResp) + return VersionedClientWithQueryAPIVersionResponse{}, err + } + return VersionedClientWithQueryAPIVersionResponse{}, nil +} + +// withQueryAPIVersionCreateRequest creates the WithQueryAPIVersion request. +func (client *VersionedClient) withQueryAPIVersionCreateRequest(ctx context.Context, _ *VersionedClientWithQueryAPIVersionOptions) (*policy.Request, error) { + urlPath := "/with-query-api-version" + req, err := runtime.NewRequest(ctx, http.MethodHead, runtime.JoinPaths(client.endpoint, urlPath)) + if err != nil { + return nil, err + } + reqQP := req.Raw().URL.Query() + reqQP.Set("api-version", version20221201Preview) + req.Raw().URL.RawQuery = strings.ReplaceAll(reqQP.Encode(), "+", "%20") + return req, nil +} +``` + +## The sub-client uses the propagated API version for its path + +```go versionedsubgroup_client +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. +// Code generated by Microsoft (R) Go Code Generator. DO NOT EDIT. + +package testmodule + +import ( + "context" + "github.com/Azure/azure-sdk-for-go/sdk/azcore" + "github.com/Azure/azure-sdk-for-go/sdk/azcore/policy" + "github.com/Azure/azure-sdk-for-go/sdk/azcore/runtime" + "net/http" + "net/url" + "strings" +) + +// VersionedSubGroupClient contains the methods for the VersionedSubGroup group. +// Don't use this type directly, use [VersionedClient.NewVersionedSubGroupClient] instead. +// +// Generated from API version 2022-12-01-preview +type VersionedSubGroupClient struct { + internal *azcore.Client + apiVersion string + endpoint string +} + +// WithPathAPIVersion - +// If the operation fails it returns an *azcore.ResponseError type. +// - options - VersionedSubGroupClientWithPathAPIVersionOptions contains the optional parameters for the VersionedSubGroupClient.WithPathAPIVersion +// method. +func (client *VersionedSubGroupClient) WithPathAPIVersion(ctx context.Context, options *VersionedSubGroupClientWithPathAPIVersionOptions) (VersionedSubGroupClientWithPathAPIVersionResponse, error) { + var err error + req, err := client.withPathAPIVersionCreateRequest(ctx, options) + if err != nil { + return VersionedSubGroupClientWithPathAPIVersionResponse{}, err + } + httpResp, err := client.internal.Pipeline().Do(req) + if err != nil { + return VersionedSubGroupClientWithPathAPIVersionResponse{}, err + } + if !runtime.HasStatusCode(httpResp, http.StatusNoContent) { + err = runtime.NewResponseError(httpResp) + return VersionedSubGroupClientWithPathAPIVersionResponse{}, err + } + return VersionedSubGroupClientWithPathAPIVersionResponse{}, nil +} + +// withPathAPIVersionCreateRequest creates the WithPathAPIVersion request. +func (client *VersionedSubGroupClient) withPathAPIVersionCreateRequest(ctx context.Context, _ *VersionedSubGroupClientWithPathAPIVersionOptions) (*policy.Request, error) { + urlPath := "/sub/with-path-api-version/{apiVersion}" + urlPath = strings.ReplaceAll(urlPath, "{apiVersion}", url.PathEscape(client.apiVersion)) + req, err := runtime.NewRequest(ctx, http.MethodHead, runtime.JoinPaths(client.endpoint, urlPath)) + if err != nil { + return nil, err + } + return req, nil +} +``` From 5b10b0262b4fb56fc23fd909d96fa7e03ad47fac Mon Sep 17 00:00:00 2001 From: tadelesh Date: Tue, 4 Aug 2026 18:06:33 +0800 Subject: [PATCH 2/6] refactor(typespec-go): preserve path API version literals Store only the configured API version override on generated clients. Path request builders now retain their generated literal and replace it only when ClientOptions.APIVersion is non-empty, including ARM and hierarchical clients. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: a2fed377-7b26-45fc-b3aa-8b6d6df8b46a --- .../src/codegen/core/operations.ts | 26 +++++ packages/typespec-go/src/codemodel/client.ts | 4 + .../typespec-go/src/tcgcadapter/clients.ts | 46 +++----- .../arm-path-api-version-override.test.ts | 7 ++ .../arm-path-api-version-override.md | 101 ++++++++++++++++++ .../scenarios/path-api-version-override.md | 12 +-- .../scenarios/subclient-api-version-policy.md | 12 +-- .../subclient-path-api-version-override.md | 12 +-- 8 files changed, 171 insertions(+), 49 deletions(-) create mode 100644 packages/typespec-go/test/unittest/scenario-suites/arm-path-api-version-override.test.ts create mode 100644 packages/typespec-go/test/unittest/scenarios/arm-path-api-version-override.md diff --git a/packages/typespec-go/src/codegen/core/operations.ts b/packages/typespec-go/src/codegen/core/operations.ts index 7a759f43c3..63dc561c80 100644 --- a/packages/typespec-go/src/codegen/core/operations.ts +++ b/packages/typespec-go/src/codegen/core/operations.ts @@ -94,9 +94,13 @@ export function generateOperations( } const indent = new helpers.Indentation(); + const pathAPIVersionOverride = client.hasPathAPIVersion; clientText += `type ${client.name} struct {\n`; clientText += `${indent.get()}internal *${azureARM ? "arm" : "azcore"}.Client\n`; + if (pathAPIVersionOverride) { + clientText += `${indent.get()}apiVersion string\n`; + } // check for any optional host params const optionalParams = new Array(); @@ -146,6 +150,9 @@ export function generateOperations( opText += `func (client *${client.name}) ${clientAccessor.name}(${getAPIParametersSig(clientAccessor, imports)}) *${subClientDecl} {\n`; opText += `${indent.get()}return &${subClientDecl}{\n`; const initFields = new Array("internal: client.internal"); + if (clientAccessor.returns.hasPathAPIVersion) { + initFields.push("apiVersion: client.apiVersion"); + } // propagate all client params for (const param of clientAccessor.parameters) { // by convention, the client accessor params have the @@ -262,6 +269,7 @@ function generateConstructors( for (const constructor of client.instance.constructors) { const ctorParams = new Array(); const paramDocs = new Array(); + let pathAPIVersionValue = "options.APIVersion"; // ctor params can also be present in the supplemental endpoint parameters const consolidatedCtorParams = new Array(); @@ -446,6 +454,13 @@ function generateConstructors( ctorText += `${indent.get()}if err != nil {\n`; ctorText += `${indent.push().get()}return nil, err\n`; ctorText += `${indent.pop().get()}}\n`; + if (client.hasPathAPIVersion && clientOptions.kind === "armClientOptions") { + pathAPIVersionValue = "pathAPIVersion"; + ctorText += `${indent.get()}${pathAPIVersionValue} := ""\n`; + ctorText += `${indent.get()}if options != nil {\n`; + ctorText += `${indent.push().get()}${pathAPIVersionValue} = options.APIVersion\n`; + ctorText += `${indent.pop().get()}}\n`; + } // handle any client-side defaults if (clientOptions.kind === "clientOptions") { @@ -497,6 +512,9 @@ function generateConstructors( // as any supplemental endpoint params are ephemeral and // consumed during client construction. indent.push(); + if (client.hasPathAPIVersion) { + ctorText += `${indent.get()}apiVersion: ${pathAPIVersionValue},\n`; + } for (const parameter of client.parameters) { if (go.isLiteralParameter(parameter.style)) { continue; @@ -1115,6 +1133,7 @@ function createProtocolRequest( const methodParamGroups = helpers.getMethodParamGroups(method); const hasPathParams = methodParamGroups.pathParams.length > 0; + const pathAPIVersionOverride = method.receiver.type.hasPathAPIVersion; // storage needs the client.u to be the source-of-truth for the full path. // however, swagger requires that all operations specify a path, which is at odds with storage. @@ -1159,6 +1178,13 @@ function createProtocolRequest( if (pp.style === "literal") { // literals are always scalar types and require no empty checks paramValue = helpers.formatParamValue(pp, imports, indent); + if (pp.kind === "pathScalarParam" && pp.isApiVersion && pathAPIVersionOverride) { + text += `${indent.get()}apiVersion := ${paramValue}\n`; + text += `${indent.get()}if client.apiVersion != "" {\n`; + text += `${indent.push().get()}apiVersion = client.apiVersion\n`; + text += `${indent.pop().get()}}\n`; + paramValue = "apiVersion"; + } } else if (pp.style === "required" || pp.location === "client") { // NOTE: we include client params here since they behave // like required params (i.e. not grouped). diff --git a/packages/typespec-go/src/codemodel/client.ts b/packages/typespec-go/src/codemodel/client.ts index 5706195e9a..b5c042f9de 100644 --- a/packages/typespec-go/src/codemodel/client.ts +++ b/packages/typespec-go/src/codemodel/client.ts @@ -47,6 +47,9 @@ export interface Client { */ apiVersions: Array; + /** indicates that this client stores an API version override for operation paths */ + hasPathAPIVersion: boolean; + /** the parent client in a hierarchical client */ parent?: Client; } @@ -397,6 +400,7 @@ export class Client implements Client { this.parameters = new Array(); this.pkg = pkg; this.apiVersions = new Array(); + this.hasPathAPIVersion = false; } } diff --git a/packages/typespec-go/src/tcgcadapter/clients.ts b/packages/typespec-go/src/tcgcadapter/clients.ts index 1e4a4915fa..13df33233b 100644 --- a/packages/typespec-go/src/tcgcadapter/clients.ts +++ b/packages/typespec-go/src/tcgcadapter/clients.ts @@ -1253,10 +1253,6 @@ export class ClientAdapter { // can be used in multiple ways. e.g. a client param "apiVersion" that's used as a path param // in one method and a query param in another. path API versions are kept separately because // they require a client field while query/header API versions are handled by the pipeline. - const isPathAPIVersionWithDefault = - adaptedParam.kind === "pathScalarParam" && - adaptedParam.isApiVersion && - go.isClientSideDefault(adaptedParam.style); const addClientParameter = (client: go.Client): void => { const existingParam = client.parameters.find((v: go.ClientParameter) => { if (v.name !== adaptedParam.name) { @@ -1293,13 +1289,6 @@ export class ClientAdapter { for (const ctor of client.instance.constructors) { ctor.parameters.push(adaptedParam); } - if ( - client.instance.options.kind === "clientOptions" && - isPathAPIVersionWithDefault && - !client.instance.options.parameters.some((param) => param.name === adaptedParam.name) - ) { - client.instance.options.parameters.push(adaptedParam); - } } }; @@ -1358,6 +1347,19 @@ export class ClientAdapter { let paramStyle: go.ParameterStyle; if (opParam.clientDefaultValue) { const client = method.receiver.type; + if (opParam.kind === "path" && opParam.onClient) { + let rootClient = client; + while (rootClient.parent) { + rootClient = rootClient.parent; + } + if (rootClient.instance?.kind === "constructable") { + let currentClient: go.Client | undefined = client; + while (currentClient) { + currentClient.hasPathAPIVersion = true; + currentClient = currentClient.parent; + } + } + } // check if we already have a ConstantDef for this API version. let versionConst = client.apiVersions.find( (e) => e.literal.literal === opParam.clientDefaultValue, @@ -1370,23 +1372,8 @@ export class ClientAdapter { ); client.apiVersions.push(versionConst); } - const versionLiteral = new go.Literal(versionConst, versionConst.name); - let rootClient = client; - while (rootClient.parent) { - rootClient = rootClient.parent; - } - if ( - opParam.kind === "path" && - opParam.onClient && - rootClient.instance?.kind === "constructable" && - rootClient.instance.options.kind === "clientOptions" - ) { - paramType = this.ta.getStringType(); - paramStyle = new go.ClientSideDefault(versionLiteral); - } else { - paramType = versionLiteral; - paramStyle = "literal"; - } + paramType = new go.Literal(versionConst, versionConst.name); + paramStyle = "literal"; } else { paramType = this.ta.getStringType(); paramStyle = opParam.optional ? "optional" : "required"; @@ -1416,9 +1403,6 @@ export class ClientAdapter { true, paramLoc, ); - if (go.isClientSideDefault(paramStyle)) { - apiVersionParam.omitEmptyStringCheck = true; - } break; case "query": apiVersionParam = new go.QueryScalarParameter( diff --git a/packages/typespec-go/test/unittest/scenario-suites/arm-path-api-version-override.test.ts b/packages/typespec-go/test/unittest/scenario-suites/arm-path-api-version-override.test.ts new file mode 100644 index 0000000000..b8fd73b8f0 --- /dev/null +++ b/packages/typespec-go/test/unittest/scenario-suites/arm-path-api-version-override.test.ts @@ -0,0 +1,7 @@ +// Generated by `pnpm gen:scenario-suites`. Do not edit by hand. +import { resolvePath } from "@typespec/compiler"; +import { describeScenarioFile } from "../scenario-runner.js"; + +describeScenarioFile( + resolvePath(import.meta.dirname, "../scenarios/arm-path-api-version-override.md"), +); diff --git a/packages/typespec-go/test/unittest/scenarios/arm-path-api-version-override.md b/packages/typespec-go/test/unittest/scenarios/arm-path-api-version-override.md new file mode 100644 index 0000000000..624125fa56 --- /dev/null +++ b/packages/typespec-go/test/unittest/scenarios/arm-path-api-version-override.md @@ -0,0 +1,101 @@ +# An ARM path API version uses the client-level override + +## TypeSpec + +```tsp +@armProviderNamespace +@versioned(Versions) +namespace Microsoft.Test; + +enum Versions { + v2022_01_01: "2022-01-01", +} + +@get +@route("/api-version/{apiVersion}") +op get(@path apiVersion: string): void; +``` + +## The generated ARM client stores the optional override and falls back to the literal + +```go client +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. +// Code generated by Microsoft (R) Go Code Generator. DO NOT EDIT. + +package testmodule + +import ( + "context" + "github.com/Azure/azure-sdk-for-go/sdk/azcore" + "github.com/Azure/azure-sdk-for-go/sdk/azcore/arm" + "github.com/Azure/azure-sdk-for-go/sdk/azcore/policy" + "github.com/Azure/azure-sdk-for-go/sdk/azcore/runtime" + "net/http" + "net/url" + "strings" +) + +// TestClient contains the methods for the Test group. +// Don't use this type directly, use NewTestClient() instead. +// +// Generated from API version 2022-01-01 +type TestClient struct { + internal *arm.Client + apiVersion string +} + +// NewTestClient creates a new instance of TestClient with the specified values. +// - credential - used to authorize requests. Usually a credential from azidentity. +// - options - Contains optional client configuration. Pass nil to accept the default values. +func NewTestClient(credential azcore.TokenCredential, options *arm.ClientOptions) (*TestClient, error) { + cl, err := arm.NewClient(moduleName, moduleVersion, credential, options) + if err != nil { + return nil, err + } + pathAPIVersion := "" + if options != nil { + pathAPIVersion = options.APIVersion + } + client := &TestClient{ + apiVersion: pathAPIVersion, + internal: cl, + } + return client, nil +} + +// Get - +// If the operation fails it returns an *azcore.ResponseError type. +// - options - TestClientGetOptions contains the optional parameters for the TestClient.Get method. +func (client *TestClient) Get(ctx context.Context, options *TestClientGetOptions) (TestClientGetResponse, error) { + var err error + req, err := client.getCreateRequest(ctx, options) + if err != nil { + return TestClientGetResponse{}, err + } + httpResp, err := client.internal.Pipeline().Do(req) + if err != nil { + return TestClientGetResponse{}, err + } + if !runtime.HasStatusCode(httpResp, http.StatusNoContent) { + err = runtime.NewResponseError(httpResp) + return TestClientGetResponse{}, err + } + return TestClientGetResponse{}, nil +} + +// getCreateRequest creates the Get request. +func (client *TestClient) getCreateRequest(ctx context.Context, _ *TestClientGetOptions) (*policy.Request, error) { + urlPath := "/api-version/{apiVersion}" + apiVersion := version20220101 + if client.apiVersion != "" { + apiVersion = client.apiVersion + } + urlPath = strings.ReplaceAll(urlPath, "{apiVersion}", url.PathEscape(apiVersion)) + req, err := runtime.NewRequest(ctx, http.MethodGet, runtime.JoinPaths(client.internal.Endpoint(), urlPath)) + if err != nil { + return nil, err + } + return req, nil +} +``` diff --git a/packages/typespec-go/test/unittest/scenarios/path-api-version-override.md b/packages/typespec-go/test/unittest/scenarios/path-api-version-override.md index 6da3d1c9b2..4a179aa99a 100644 --- a/packages/typespec-go/test/unittest/scenarios/path-api-version-override.md +++ b/packages/typespec-go/test/unittest/scenarios/path-api-version-override.md @@ -77,12 +77,8 @@ func NewVersionedClientWithNoCredential(endpoint string, options *VersionedClien if err != nil { return nil, err } - apiVersion := version20221201Preview - if options.APIVersion != "" { - apiVersion = options.APIVersion - } client := &VersionedClient{ - apiVersion: apiVersion, + apiVersion: options.APIVersion, endpoint: endpoint, internal: cl, } @@ -113,7 +109,11 @@ func (client *VersionedClient) WithPathAPIVersion(ctx context.Context, options * // withPathAPIVersionCreateRequest creates the WithPathAPIVersion request. func (client *VersionedClient) withPathAPIVersionCreateRequest(ctx context.Context, _ *VersionedClientWithPathAPIVersionOptions) (*policy.Request, error) { urlPath := "/with-path-api-version/{apiVersion}" - urlPath = strings.ReplaceAll(urlPath, "{apiVersion}", url.PathEscape(client.apiVersion)) + apiVersion := version20221201Preview + if client.apiVersion != "" { + apiVersion = client.apiVersion + } + urlPath = strings.ReplaceAll(urlPath, "{apiVersion}", url.PathEscape(apiVersion)) req, err := runtime.NewRequest(ctx, http.MethodHead, runtime.JoinPaths(client.endpoint, urlPath)) if err != nil { return nil, err diff --git a/packages/typespec-go/test/unittest/scenarios/subclient-api-version-policy.md b/packages/typespec-go/test/unittest/scenarios/subclient-api-version-policy.md index b3a97e080f..e29037160c 100644 --- a/packages/typespec-go/test/unittest/scenarios/subclient-api-version-policy.md +++ b/packages/typespec-go/test/unittest/scenarios/subclient-api-version-policy.md @@ -80,12 +80,8 @@ func NewVersionedClientWithNoCredential(endpoint string, options *VersionedClien if err != nil { return nil, err } - apiVersion := version20221201Preview - if options.APIVersion != "" { - apiVersion = options.APIVersion - } client := &VersionedClient{ - apiVersion: apiVersion, + apiVersion: options.APIVersion, endpoint: endpoint, internal: cl, } @@ -124,7 +120,11 @@ func (client *VersionedClient) WithPathAPIVersion(ctx context.Context, options * // withPathAPIVersionCreateRequest creates the WithPathAPIVersion request. func (client *VersionedClient) withPathAPIVersionCreateRequest(ctx context.Context, _ *VersionedClientWithPathAPIVersionOptions) (*policy.Request, error) { urlPath := "/with-path-api-version/{apiVersion}" - urlPath = strings.ReplaceAll(urlPath, "{apiVersion}", url.PathEscape(client.apiVersion)) + apiVersion := version20221201Preview + if client.apiVersion != "" { + apiVersion = client.apiVersion + } + urlPath = strings.ReplaceAll(urlPath, "{apiVersion}", url.PathEscape(apiVersion)) req, err := runtime.NewRequest(ctx, http.MethodHead, runtime.JoinPaths(client.endpoint, urlPath)) if err != nil { return nil, err diff --git a/packages/typespec-go/test/unittest/scenarios/subclient-path-api-version-override.md b/packages/typespec-go/test/unittest/scenarios/subclient-path-api-version-override.md index cfeb3871ba..11d9cfc578 100644 --- a/packages/typespec-go/test/unittest/scenarios/subclient-path-api-version-override.md +++ b/packages/typespec-go/test/unittest/scenarios/subclient-path-api-version-override.md @@ -79,12 +79,8 @@ func NewVersionedClientWithNoCredential(endpoint string, options *VersionedClien if err != nil { return nil, err } - apiVersion := version20221201Preview - if options.APIVersion != "" { - apiVersion = options.APIVersion - } client := &VersionedClient{ - apiVersion: apiVersion, + apiVersion: options.APIVersion, endpoint: endpoint, internal: cl, } @@ -188,7 +184,11 @@ func (client *VersionedSubGroupClient) WithPathAPIVersion(ctx context.Context, o // withPathAPIVersionCreateRequest creates the WithPathAPIVersion request. func (client *VersionedSubGroupClient) withPathAPIVersionCreateRequest(ctx context.Context, _ *VersionedSubGroupClientWithPathAPIVersionOptions) (*policy.Request, error) { urlPath := "/sub/with-path-api-version/{apiVersion}" - urlPath = strings.ReplaceAll(urlPath, "{apiVersion}", url.PathEscape(client.apiVersion)) + apiVersion := version20221201Preview + if client.apiVersion != "" { + apiVersion = client.apiVersion + } + urlPath = strings.ReplaceAll(urlPath, "{apiVersion}", url.PathEscape(apiVersion)) req, err := runtime.NewRequest(ctx, http.MethodHead, runtime.JoinPaths(client.endpoint, urlPath)) if err != nil { return nil, err From 7ebbbe46e8781a94ab506f1816168207d28b9f43 Mon Sep 17 00:00:00 2001 From: tadelesh Date: Wed, 5 Aug 2026 13:17:50 +0800 Subject: [PATCH 3/6] refactor(typespec-go): simplify API version propagation Normalize ARM options before reading APIVersion, keep literal API-version metadata on the constructable client, and restore the Azure path Spector request test. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: a2fed377-7b26-45fc-b3aa-8b6d6df8b46a --- .../src/codegen/core/operations.ts | 35 ++++++++----------- .../typespec-go/src/tcgcadapter/clients.ts | 18 ++++++---- .../apiversionpathgroup/path_client_test.go | 11 +++++- .../arm-path-api-version-override.md | 9 +++-- 4 files changed, 40 insertions(+), 33 deletions(-) diff --git a/packages/typespec-go/src/codegen/core/operations.ts b/packages/typespec-go/src/codegen/core/operations.ts index 63dc561c80..ebbd247ef2 100644 --- a/packages/typespec-go/src/codegen/core/operations.ts +++ b/packages/typespec-go/src/codegen/core/operations.ts @@ -94,11 +94,10 @@ export function generateOperations( } const indent = new helpers.Indentation(); - const pathAPIVersionOverride = client.hasPathAPIVersion; clientText += `type ${client.name} struct {\n`; clientText += `${indent.get()}internal *${azureARM ? "arm" : "azcore"}.Client\n`; - if (pathAPIVersionOverride) { + if (client.hasPathAPIVersion) { clientText += `${indent.get()}apiVersion string\n`; } @@ -165,11 +164,7 @@ export function generateOperations( for (const param of client.parameters) { if (go.isLiteralParameter(param.style)) { continue; - } else if ( - clientAccessor.returns.parameters.some( - (p) => p.name === param.name && !go.isLiteralParameter(p.style), - ) - ) { + } else if (clientAccessor.returns.parameters.some((p) => p.name === param.name)) { // only propagate ctor params that are common between parent/child initFields.push(`${param.name}: client.${param.name}`); } @@ -244,6 +239,12 @@ function generateConstructors( const clientOptions = client.instance.options; let ctorText = ""; + const emitDefaultOptions = (optionsTypeName: string): string => { + let text = `${indent.get()}if options == nil {\n`; + text += `${indent.push().get()}options = &${optionsTypeName}{}\n`; + text += `${indent.pop().get()}}\n`; + return text; + }; if (clientOptions.kind === "clientOptions") { // for non-ARM, the options type will always be a parameter group @@ -269,7 +270,6 @@ function generateConstructors( for (const constructor of client.instance.constructors) { const ctorParams = new Array(); const paramDocs = new Array(); - let pathAPIVersionValue = "options.APIVersion"; // ctor params can also be present in the supplemental endpoint parameters const consolidatedCtorParams = new Array(); @@ -306,9 +306,7 @@ function generateConstructors( plOpts?: string, ): string { imports.add("github.com/Azure/azure-sdk-for-go/sdk/azcore/runtime"); - let bodyText = `${indent.get()}if options == nil {\n`; - bodyText += `${indent.push().get()}options = &${optionsTypeName}{}\n`; - bodyText += `${indent.pop().get()}}\n`; + let bodyText = emitDefaultOptions(optionsTypeName); let apiVersionConfig = ""; // check if there's an api version parameter let apiVersionParam: @@ -427,7 +425,11 @@ function generateConstructors( case "armClientOptions": // this is the ARM case imports.add("github.com/Azure/azure-sdk-for-go/sdk/azcore/arm"); - prolog = `${indent.get()}cl, err := arm.NewClient(moduleName, moduleVersion, credential, options)\n`; + prolog = ""; + if (client.hasPathAPIVersion) { + prolog += emitDefaultOptions(go.getTypeDeclaration(clientOptions, client.pkg)); + } + prolog += `${indent.get()}cl, err := arm.NewClient(moduleName, moduleVersion, credential, options)\n`; break; } break; @@ -454,13 +456,6 @@ function generateConstructors( ctorText += `${indent.get()}if err != nil {\n`; ctorText += `${indent.push().get()}return nil, err\n`; ctorText += `${indent.pop().get()}}\n`; - if (client.hasPathAPIVersion && clientOptions.kind === "armClientOptions") { - pathAPIVersionValue = "pathAPIVersion"; - ctorText += `${indent.get()}${pathAPIVersionValue} := ""\n`; - ctorText += `${indent.get()}if options != nil {\n`; - ctorText += `${indent.push().get()}${pathAPIVersionValue} = options.APIVersion\n`; - ctorText += `${indent.pop().get()}}\n`; - } // handle any client-side defaults if (clientOptions.kind === "clientOptions") { @@ -513,7 +508,7 @@ function generateConstructors( // consumed during client construction. indent.push(); if (client.hasPathAPIVersion) { - ctorText += `${indent.get()}apiVersion: ${pathAPIVersionValue},\n`; + ctorText += `${indent.get()}apiVersion: options.APIVersion,\n`; } for (const parameter of client.parameters) { if (go.isLiteralParameter(parameter.style)) { diff --git a/packages/typespec-go/src/tcgcadapter/clients.ts b/packages/typespec-go/src/tcgcadapter/clients.ts index 13df33233b..08663f9c09 100644 --- a/packages/typespec-go/src/tcgcadapter/clients.ts +++ b/packages/typespec-go/src/tcgcadapter/clients.ts @@ -1292,14 +1292,18 @@ export class ClientAdapter { } }; - addClientParameter(method.receiver.type); - if (go.isAPIVersionParameter(adaptedParam)) { - let parent = method.receiver.type.parent; - while (parent) { - addClientParameter(parent); - parent = parent.parent; + const isApiVersion = go.isAPIVersionParameter(adaptedParam); + const isLiteralApiVersion = isApiVersion && go.isLiteralParameter(adaptedParam.style); + let client: go.Client | undefined = method.receiver.type; + if (isLiteralApiVersion) { + while (client.instance?.kind !== "constructable" && client.parent) { + client = client.parent; } } + while (client) { + addClientParameter(client); + client = isApiVersion && !isLiteralApiVersion ? client.parent : undefined; + } } } @@ -1341,7 +1345,7 @@ export class ClientAdapter { ): go.MethodParameter { if (opParam.isApiVersionParam) { // Header/query API versions are emitted inline and overridden by the pipeline. - // Path API versions must be stored on the client because the pipeline cannot + // Path API version overrides must be stored on the client because the pipeline cannot // replace a path segment after the request URL has been constructed. let paramType: go.Literal | go.String; let paramStyle: go.ParameterStyle; diff --git a/packages/typespec-go/test/azure-http-specs/azure/client-generator-core/api-version/apiversionpathgroup/path_client_test.go b/packages/typespec-go/test/azure-http-specs/azure/client-generator-core/api-version/apiversionpathgroup/path_client_test.go index 3c63b90d40..002b36b790 100644 --- a/packages/typespec-go/test/azure-http-specs/azure/client-generator-core/api-version/apiversionpathgroup/path_client_test.go +++ b/packages/typespec-go/test/azure-http-specs/azure/client-generator-core/api-version/apiversionpathgroup/path_client_test.go @@ -4,9 +4,18 @@ package apiversionpathgroup_test import ( + "context" "testing" + + "apiversionpathgroup" + "github.com/stretchr/testify/require" ) func TestPathClient_PathAPIVersion(t *testing.T) { - t.Skip("https://github.com/Azure/autorest.go/issues/1743") + client, err := apiversionpathgroup.NewPathClientWithNoCredential("http://localhost:3000", nil) + require.NoError(t, err) + + resp, err := client.PathAPIVersion(context.Background(), nil) + require.NoError(t, err) + require.Zero(t, resp) } diff --git a/packages/typespec-go/test/unittest/scenarios/arm-path-api-version-override.md b/packages/typespec-go/test/unittest/scenarios/arm-path-api-version-override.md index 624125fa56..fbc131e7db 100644 --- a/packages/typespec-go/test/unittest/scenarios/arm-path-api-version-override.md +++ b/packages/typespec-go/test/unittest/scenarios/arm-path-api-version-override.md @@ -49,16 +49,15 @@ type TestClient struct { // - credential - used to authorize requests. Usually a credential from azidentity. // - options - Contains optional client configuration. Pass nil to accept the default values. func NewTestClient(credential azcore.TokenCredential, options *arm.ClientOptions) (*TestClient, error) { + if options == nil { + options = &arm.ClientOptions{} + } cl, err := arm.NewClient(moduleName, moduleVersion, credential, options) if err != nil { return nil, err } - pathAPIVersion := "" - if options != nil { - pathAPIVersion = options.APIVersion - } client := &TestClient{ - apiVersion: pathAPIVersion, + apiVersion: options.APIVersion, internal: cl, } return client, nil From 3c764c5df8358e293947e1def49db246636af312 Mon Sep 17 00:00:00 2001 From: tadelesh Date: Wed, 5 Aug 2026 17:17:32 +0800 Subject: [PATCH 4/6] refactor(typespec-go): use first API version parameter Assume a client uses one API-version location, select its first API-version parameter, and remove mixed-location scenario coverage. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: a2fed377-7b26-45fc-b3aa-8b6d6df8b46a --- .../src/codegen/core/operations.ts | 8 +- .../typespec-go/src/tcgcadapter/clients.ts | 21 +- .../subclient-api-version-policy.test.ts | 7 - .../scenarios/path-api-version-override.md | 41 +--- .../scenarios/subclient-api-version-policy.md | 196 ------------------ .../subclient-path-api-version-override.md | 47 +---- 6 files changed, 8 insertions(+), 312 deletions(-) delete mode 100644 packages/typespec-go/test/unittest/scenario-suites/subclient-api-version-policy.test.ts delete mode 100644 packages/typespec-go/test/unittest/scenarios/subclient-api-version-policy.md diff --git a/packages/typespec-go/src/codegen/core/operations.ts b/packages/typespec-go/src/codegen/core/operations.ts index ebbd247ef2..f8fd88d1e8 100644 --- a/packages/typespec-go/src/codegen/core/operations.ts +++ b/packages/typespec-go/src/codegen/core/operations.ts @@ -322,13 +322,7 @@ function generateConstructors( case "queryScalarParam": case "uriParam": if (param.isApiVersion) { - const currentIsPath = - apiVersionParam?.kind === "pathScalarParam" || - apiVersionParam?.kind === "uriParam"; - const paramIsPath = param.kind === "pathScalarParam" || param.kind === "uriParam"; - if (!apiVersionParam || (currentIsPath && !paramIsPath)) { - apiVersionParam = param; - } + apiVersionParam ??= param; } } } diff --git a/packages/typespec-go/src/tcgcadapter/clients.ts b/packages/typespec-go/src/tcgcadapter/clients.ts index 08663f9c09..e22d0492ed 100644 --- a/packages/typespec-go/src/tcgcadapter/clients.ts +++ b/packages/typespec-go/src/tcgcadapter/clients.ts @@ -1249,23 +1249,12 @@ export class ClientAdapter { continue; } - // we must check via param name and not reference equality. this is because a client param - // can be used in multiple ways. e.g. a client param "apiVersion" that's used as a path param - // in one method and a query param in another. path API versions are kept separately because - // they require a client field while query/header API versions are handled by the pipeline. + // check via param name and not reference equality as a client param can be + // referenced by multiple methods. const addClientParameter = (client: go.Client): void => { - const existingParam = client.parameters.find((v: go.ClientParameter) => { - if (v.name !== adaptedParam.name) { - return false; - } - if (!go.isAPIVersionParameter(v) || !go.isAPIVersionParameter(adaptedParam)) { - return true; - } - return ( - v.kind === adaptedParam.kind || - (v.kind !== "pathScalarParam" && adaptedParam.kind !== "pathScalarParam") - ); - }); + const existingParam = client.parameters.find( + (v: go.ClientParameter) => v.name === adaptedParam.name, + ); if (existingParam) { return; } diff --git a/packages/typespec-go/test/unittest/scenario-suites/subclient-api-version-policy.test.ts b/packages/typespec-go/test/unittest/scenario-suites/subclient-api-version-policy.test.ts deleted file mode 100644 index 703329259e..0000000000 --- a/packages/typespec-go/test/unittest/scenario-suites/subclient-api-version-policy.test.ts +++ /dev/null @@ -1,7 +0,0 @@ -// Generated by `pnpm gen:scenario-suites`. Do not edit by hand. -import { resolvePath } from "@typespec/compiler"; -import { describeScenarioFile } from "../scenario-runner.js"; - -describeScenarioFile( - resolvePath(import.meta.dirname, "../scenarios/subclient-api-version-policy.md"), -); diff --git a/packages/typespec-go/test/unittest/scenarios/path-api-version-override.md b/packages/typespec-go/test/unittest/scenarios/path-api-version-override.md index 4a179aa99a..dd62a19637 100644 --- a/packages/typespec-go/test/unittest/scenarios/path-api-version-override.md +++ b/packages/typespec-go/test/unittest/scenarios/path-api-version-override.md @@ -18,10 +18,6 @@ enum Versions { v2022_12_01_preview: "2022-12-01-preview", } -@head -@route("/with-query-api-version") -op withQueryApiVersion(@query("api-version") apiVersion: string): void; - @head @route("/with-path-api-version/{apiVersion}") op withPathApiVersion(@path apiVersion: string): void; @@ -70,8 +66,7 @@ func NewVersionedClientWithNoCredential(endpoint string, options *VersionedClien } cl, err := azcore.NewClient(moduleName, moduleVersion, runtime.PipelineOptions{ APIVersion: runtime.APIVersionOptions{ - Name: "api-version", - Location: runtime.APIVersionLocationQueryParam, + Location: runtime.APIVersionLocationPath, }, }, &options.ClientOptions) if err != nil { @@ -120,38 +115,4 @@ func (client *VersionedClient) withPathAPIVersionCreateRequest(ctx context.Conte } return req, nil } - -// WithQueryAPIVersion - -// If the operation fails it returns an *azcore.ResponseError type. -// - options - VersionedClientWithQueryAPIVersionOptions contains the optional parameters for the VersionedClient.WithQueryAPIVersion -// method. -func (client *VersionedClient) WithQueryAPIVersion(ctx context.Context, options *VersionedClientWithQueryAPIVersionOptions) (VersionedClientWithQueryAPIVersionResponse, error) { - var err error - req, err := client.withQueryAPIVersionCreateRequest(ctx, options) - if err != nil { - return VersionedClientWithQueryAPIVersionResponse{}, err - } - httpResp, err := client.internal.Pipeline().Do(req) - if err != nil { - return VersionedClientWithQueryAPIVersionResponse{}, err - } - if !runtime.HasStatusCode(httpResp, http.StatusNoContent) { - err = runtime.NewResponseError(httpResp) - return VersionedClientWithQueryAPIVersionResponse{}, err - } - return VersionedClientWithQueryAPIVersionResponse{}, nil -} - -// withQueryAPIVersionCreateRequest creates the WithQueryAPIVersion request. -func (client *VersionedClient) withQueryAPIVersionCreateRequest(ctx context.Context, _ *VersionedClientWithQueryAPIVersionOptions) (*policy.Request, error) { - urlPath := "/with-query-api-version" - req, err := runtime.NewRequest(ctx, http.MethodHead, runtime.JoinPaths(client.endpoint, urlPath)) - if err != nil { - return nil, err - } - reqQP := req.Raw().URL.Query() - reqQP.Set("api-version", version20221201Preview) - req.Raw().URL.RawQuery = strings.ReplaceAll(reqQP.Encode(), "+", "%20") - return req, nil -} ``` diff --git a/packages/typespec-go/test/unittest/scenarios/subclient-api-version-policy.md b/packages/typespec-go/test/unittest/scenarios/subclient-api-version-policy.md deleted file mode 100644 index e29037160c..0000000000 --- a/packages/typespec-go/test/unittest/scenarios/subclient-api-version-policy.md +++ /dev/null @@ -1,196 +0,0 @@ -# A sub-client query API version keeps the root client's query policy - -## TypeSpec - -```tsp -@service -@versioned(Versions) -@server( - "{endpoint}", - "Test endpoint", - { - endpoint: url, - } -) -namespace Versioned; - -enum Versions { - v2022_12_01_preview: "2022-12-01-preview", -} - -@head -@route("/with-path-api-version/{apiVersion}") -op withPathApiVersion(@path apiVersion: string): void; - -@route("/sub") -interface SubGroup { - @head - @route("/with-query-api-version") - withQueryApiVersion(@query("api-version") apiVersion: string): void; -} -``` - -## The root client keeps its path field and configures the query policy - -```go versioned_client -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. See License.txt in the project root for license information. -// Code generated by Microsoft (R) Go Code Generator. DO NOT EDIT. - -package testmodule - -import ( - "context" - "github.com/Azure/azure-sdk-for-go/sdk/azcore" - "github.com/Azure/azure-sdk-for-go/sdk/azcore/policy" - "github.com/Azure/azure-sdk-for-go/sdk/azcore/runtime" - "net/http" - "net/url" - "strings" -) - -// VersionedClient contains the methods for the Versioned group. -// Don't use this type directly, use NewVersionedClientWithNoCredential() instead. -// -// Generated from API version 2022-12-01-preview -type VersionedClient struct { - internal *azcore.Client - apiVersion string - endpoint string -} - -// VersionedClientOptions contains the optional values for creating a [VersionedClient]. -type VersionedClientOptions struct { - azcore.ClientOptions -} - -// NewVersionedClientWithNoCredential creates a new instance of VersionedClient with the specified values. -// - endpoint - Service host -// - options - Contains optional client configuration. Pass nil to accept the default values. -func NewVersionedClientWithNoCredential(endpoint string, options *VersionedClientOptions) (*VersionedClient, error) { - if options == nil { - options = &VersionedClientOptions{} - } - cl, err := azcore.NewClient(moduleName, moduleVersion, runtime.PipelineOptions{ - APIVersion: runtime.APIVersionOptions{ - Name: "api-version", - Location: runtime.APIVersionLocationQueryParam, - }, - }, &options.ClientOptions) - if err != nil { - return nil, err - } - client := &VersionedClient{ - apiVersion: options.APIVersion, - endpoint: endpoint, - internal: cl, - } - return client, nil -} - -// NewVersionedSubGroupClient creates a new instance of [VersionedSubGroupClient]. -func (client *VersionedClient) NewVersionedSubGroupClient() *VersionedSubGroupClient { - return &VersionedSubGroupClient{ - endpoint: client.endpoint, - internal: client.internal, - } -} - -// WithPathAPIVersion - -// If the operation fails it returns an *azcore.ResponseError type. -// - options - VersionedClientWithPathAPIVersionOptions contains the optional parameters for the VersionedClient.WithPathAPIVersion -// method. -func (client *VersionedClient) WithPathAPIVersion(ctx context.Context, options *VersionedClientWithPathAPIVersionOptions) (VersionedClientWithPathAPIVersionResponse, error) { - var err error - req, err := client.withPathAPIVersionCreateRequest(ctx, options) - if err != nil { - return VersionedClientWithPathAPIVersionResponse{}, err - } - httpResp, err := client.internal.Pipeline().Do(req) - if err != nil { - return VersionedClientWithPathAPIVersionResponse{}, err - } - if !runtime.HasStatusCode(httpResp, http.StatusNoContent) { - err = runtime.NewResponseError(httpResp) - return VersionedClientWithPathAPIVersionResponse{}, err - } - return VersionedClientWithPathAPIVersionResponse{}, nil -} - -// withPathAPIVersionCreateRequest creates the WithPathAPIVersion request. -func (client *VersionedClient) withPathAPIVersionCreateRequest(ctx context.Context, _ *VersionedClientWithPathAPIVersionOptions) (*policy.Request, error) { - urlPath := "/with-path-api-version/{apiVersion}" - apiVersion := version20221201Preview - if client.apiVersion != "" { - apiVersion = client.apiVersion - } - urlPath = strings.ReplaceAll(urlPath, "{apiVersion}", url.PathEscape(apiVersion)) - req, err := runtime.NewRequest(ctx, http.MethodHead, runtime.JoinPaths(client.endpoint, urlPath)) - if err != nil { - return nil, err - } - return req, nil -} -``` - -## The query-only sub-client does not receive a nonexistent API version field - -```go versionedsubgroup_client -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. See License.txt in the project root for license information. -// Code generated by Microsoft (R) Go Code Generator. DO NOT EDIT. - -package testmodule - -import ( - "context" - "github.com/Azure/azure-sdk-for-go/sdk/azcore" - "github.com/Azure/azure-sdk-for-go/sdk/azcore/policy" - "github.com/Azure/azure-sdk-for-go/sdk/azcore/runtime" - "net/http" - "strings" -) - -// VersionedSubGroupClient contains the methods for the VersionedSubGroup group. -// Don't use this type directly, use [VersionedClient.NewVersionedSubGroupClient] instead. -// -// Generated from API version 2022-12-01-preview -type VersionedSubGroupClient struct { - internal *azcore.Client - endpoint string -} - -// WithQueryAPIVersion - -// If the operation fails it returns an *azcore.ResponseError type. -// - options - VersionedSubGroupClientWithQueryAPIVersionOptions contains the optional parameters for the VersionedSubGroupClient.WithQueryAPIVersion -// method. -func (client *VersionedSubGroupClient) WithQueryAPIVersion(ctx context.Context, options *VersionedSubGroupClientWithQueryAPIVersionOptions) (VersionedSubGroupClientWithQueryAPIVersionResponse, error) { - var err error - req, err := client.withQueryAPIVersionCreateRequest(ctx, options) - if err != nil { - return VersionedSubGroupClientWithQueryAPIVersionResponse{}, err - } - httpResp, err := client.internal.Pipeline().Do(req) - if err != nil { - return VersionedSubGroupClientWithQueryAPIVersionResponse{}, err - } - if !runtime.HasStatusCode(httpResp, http.StatusNoContent) { - err = runtime.NewResponseError(httpResp) - return VersionedSubGroupClientWithQueryAPIVersionResponse{}, err - } - return VersionedSubGroupClientWithQueryAPIVersionResponse{}, nil -} - -// withQueryAPIVersionCreateRequest creates the WithQueryAPIVersion request. -func (client *VersionedSubGroupClient) withQueryAPIVersionCreateRequest(ctx context.Context, _ *VersionedSubGroupClientWithQueryAPIVersionOptions) (*policy.Request, error) { - urlPath := "/sub/with-query-api-version" - req, err := runtime.NewRequest(ctx, http.MethodHead, runtime.JoinPaths(client.endpoint, urlPath)) - if err != nil { - return nil, err - } - reqQP := req.Raw().URL.Query() - reqQP.Set("api-version", version20221201Preview) - req.Raw().URL.RawQuery = strings.ReplaceAll(reqQP.Encode(), "+", "%20") - return req, nil -} -``` diff --git a/packages/typespec-go/test/unittest/scenarios/subclient-path-api-version-override.md b/packages/typespec-go/test/unittest/scenarios/subclient-path-api-version-override.md index 11d9cfc578..36b75cfe3a 100644 --- a/packages/typespec-go/test/unittest/scenarios/subclient-path-api-version-override.md +++ b/packages/typespec-go/test/unittest/scenarios/subclient-path-api-version-override.md @@ -18,10 +18,6 @@ enum Versions { v2022_12_01_preview: "2022-12-01-preview", } -@head -@route("/with-query-api-version") -op withQueryApiVersion(@query("api-version") apiVersion: string): void; - @route("/sub") interface SubGroup { @head @@ -40,18 +36,12 @@ interface SubGroup { package testmodule import ( - "context" "github.com/Azure/azure-sdk-for-go/sdk/azcore" - "github.com/Azure/azure-sdk-for-go/sdk/azcore/policy" "github.com/Azure/azure-sdk-for-go/sdk/azcore/runtime" - "net/http" - "strings" ) // VersionedClient contains the methods for the Versioned group. // Don't use this type directly, use NewVersionedClientWithNoCredential() instead. -// -// Generated from API version 2022-12-01-preview type VersionedClient struct { internal *azcore.Client apiVersion string @@ -72,8 +62,7 @@ func NewVersionedClientWithNoCredential(endpoint string, options *VersionedClien } cl, err := azcore.NewClient(moduleName, moduleVersion, runtime.PipelineOptions{ APIVersion: runtime.APIVersionOptions{ - Name: "api-version", - Location: runtime.APIVersionLocationQueryParam, + Location: runtime.APIVersionLocationPath, }, }, &options.ClientOptions) if err != nil { @@ -95,40 +84,6 @@ func (client *VersionedClient) NewVersionedSubGroupClient() *VersionedSubGroupCl internal: client.internal, } } - -// WithQueryAPIVersion - -// If the operation fails it returns an *azcore.ResponseError type. -// - options - VersionedClientWithQueryAPIVersionOptions contains the optional parameters for the VersionedClient.WithQueryAPIVersion -// method. -func (client *VersionedClient) WithQueryAPIVersion(ctx context.Context, options *VersionedClientWithQueryAPIVersionOptions) (VersionedClientWithQueryAPIVersionResponse, error) { - var err error - req, err := client.withQueryAPIVersionCreateRequest(ctx, options) - if err != nil { - return VersionedClientWithQueryAPIVersionResponse{}, err - } - httpResp, err := client.internal.Pipeline().Do(req) - if err != nil { - return VersionedClientWithQueryAPIVersionResponse{}, err - } - if !runtime.HasStatusCode(httpResp, http.StatusNoContent) { - err = runtime.NewResponseError(httpResp) - return VersionedClientWithQueryAPIVersionResponse{}, err - } - return VersionedClientWithQueryAPIVersionResponse{}, nil -} - -// withQueryAPIVersionCreateRequest creates the WithQueryAPIVersion request. -func (client *VersionedClient) withQueryAPIVersionCreateRequest(ctx context.Context, _ *VersionedClientWithQueryAPIVersionOptions) (*policy.Request, error) { - urlPath := "/with-query-api-version" - req, err := runtime.NewRequest(ctx, http.MethodHead, runtime.JoinPaths(client.endpoint, urlPath)) - if err != nil { - return nil, err - } - reqQP := req.Raw().URL.Query() - reqQP.Set("api-version", version20221201Preview) - req.Raw().URL.RawQuery = strings.ReplaceAll(reqQP.Encode(), "+", "%20") - return req, nil -} ``` ## The sub-client uses the propagated API version for its path From 73a3762147a7289f98d195c75112851bbc473ff2 Mon Sep 17 00:00:00 2001 From: tadelesh Date: Wed, 5 Aug 2026 17:41:43 +0800 Subject: [PATCH 5/6] refactor(typespec-go): narrow path API version handling Keep existing client parameter collection unchanged and limit path override marking to the current constructable client. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: a2fed377-7b26-45fc-b3aa-8b6d6df8b46a --- .../typespec-go/src/tcgcadapter/clients.ts | 56 ++----- ...ubclient-path-api-version-override.test.ts | 7 - .../subclient-path-api-version-override.md | 153 ------------------ 3 files changed, 17 insertions(+), 199 deletions(-) delete mode 100644 packages/typespec-go/test/unittest/scenario-suites/subclient-path-api-version-override.test.ts delete mode 100644 packages/typespec-go/test/unittest/scenarios/subclient-path-api-version-override.md diff --git a/packages/typespec-go/src/tcgcadapter/clients.ts b/packages/typespec-go/src/tcgcadapter/clients.ts index e22d0492ed..2531b223c6 100644 --- a/packages/typespec-go/src/tcgcadapter/clients.ts +++ b/packages/typespec-go/src/tcgcadapter/clients.ts @@ -1249,16 +1249,14 @@ export class ClientAdapter { continue; } - // check via param name and not reference equality as a client param can be - // referenced by multiple methods. - const addClientParameter = (client: go.Client): void => { - const existingParam = client.parameters.find( - (v: go.ClientParameter) => v.name === adaptedParam.name, - ); - if (existingParam) { - return; - } - + // we must check via param name and not reference equality. this is because a client param + // can be used in multiple ways. e.g. a client param "apiVersion" that's used as a path param + // in one method and a query param in another. + if ( + !method.receiver.type.parameters.find((v: go.ClientParameter) => { + return v.name === adaptedParam.name; + }) + ) { if ( this.ta.codeModel.type === "azure-arm" && adaptedParam.style !== "literal" && @@ -1270,28 +1268,14 @@ export class ClientAdapter { opParam.__raw?.node, ); } - - client.parameters.push(adaptedParam); - if (client.instance?.kind === "constructable") { + method.receiver.type.parameters.push(adaptedParam); + if (method.receiver.type.instance?.kind === "constructable") { // if this is an instantiable client then also add // the client parameter to all constructors - for (const ctor of client.instance.constructors) { + for (const ctor of method.receiver.type.instance.constructors) { ctor.parameters.push(adaptedParam); } } - }; - - const isApiVersion = go.isAPIVersionParameter(adaptedParam); - const isLiteralApiVersion = isApiVersion && go.isLiteralParameter(adaptedParam.style); - let client: go.Client | undefined = method.receiver.type; - if (isLiteralApiVersion) { - while (client.instance?.kind !== "constructable" && client.parent) { - client = client.parent; - } - } - while (client) { - addClientParameter(client); - client = isApiVersion && !isLiteralApiVersion ? client.parent : undefined; } } } @@ -1340,18 +1324,12 @@ export class ClientAdapter { let paramStyle: go.ParameterStyle; if (opParam.clientDefaultValue) { const client = method.receiver.type; - if (opParam.kind === "path" && opParam.onClient) { - let rootClient = client; - while (rootClient.parent) { - rootClient = rootClient.parent; - } - if (rootClient.instance?.kind === "constructable") { - let currentClient: go.Client | undefined = client; - while (currentClient) { - currentClient.hasPathAPIVersion = true; - currentClient = currentClient.parent; - } - } + if ( + opParam.kind === "path" && + opParam.onClient && + client.instance?.kind === "constructable" + ) { + client.hasPathAPIVersion = true; } // check if we already have a ConstantDef for this API version. let versionConst = client.apiVersions.find( diff --git a/packages/typespec-go/test/unittest/scenario-suites/subclient-path-api-version-override.test.ts b/packages/typespec-go/test/unittest/scenario-suites/subclient-path-api-version-override.test.ts deleted file mode 100644 index 7d293e2fe4..0000000000 --- a/packages/typespec-go/test/unittest/scenario-suites/subclient-path-api-version-override.test.ts +++ /dev/null @@ -1,7 +0,0 @@ -// Generated by `pnpm gen:scenario-suites`. Do not edit by hand. -import { resolvePath } from "@typespec/compiler"; -import { describeScenarioFile } from "../scenario-runner.js"; - -describeScenarioFile( - resolvePath(import.meta.dirname, "../scenarios/subclient-path-api-version-override.md"), -); diff --git a/packages/typespec-go/test/unittest/scenarios/subclient-path-api-version-override.md b/packages/typespec-go/test/unittest/scenarios/subclient-path-api-version-override.md deleted file mode 100644 index 36b75cfe3a..0000000000 --- a/packages/typespec-go/test/unittest/scenarios/subclient-path-api-version-override.md +++ /dev/null @@ -1,153 +0,0 @@ -# A sub-client inherits the client-level API version override for path parameters - -## TypeSpec - -```tsp -@service -@versioned(Versions) -@server( - "{endpoint}", - "Test endpoint", - { - endpoint: url, - } -) -namespace Versioned; - -enum Versions { - v2022_12_01_preview: "2022-12-01-preview", -} - -@route("/sub") -interface SubGroup { - @head - @route("/with-path-api-version/{apiVersion}") - withPathApiVersion(@path apiVersion: string): void; -} -``` - -## The root client stores and propagates the API version - -```go versioned_client -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. See License.txt in the project root for license information. -// Code generated by Microsoft (R) Go Code Generator. DO NOT EDIT. - -package testmodule - -import ( - "github.com/Azure/azure-sdk-for-go/sdk/azcore" - "github.com/Azure/azure-sdk-for-go/sdk/azcore/runtime" -) - -// VersionedClient contains the methods for the Versioned group. -// Don't use this type directly, use NewVersionedClientWithNoCredential() instead. -type VersionedClient struct { - internal *azcore.Client - apiVersion string - endpoint string -} - -// VersionedClientOptions contains the optional values for creating a [VersionedClient]. -type VersionedClientOptions struct { - azcore.ClientOptions -} - -// NewVersionedClientWithNoCredential creates a new instance of VersionedClient with the specified values. -// - endpoint - Service host -// - options - Contains optional client configuration. Pass nil to accept the default values. -func NewVersionedClientWithNoCredential(endpoint string, options *VersionedClientOptions) (*VersionedClient, error) { - if options == nil { - options = &VersionedClientOptions{} - } - cl, err := azcore.NewClient(moduleName, moduleVersion, runtime.PipelineOptions{ - APIVersion: runtime.APIVersionOptions{ - Location: runtime.APIVersionLocationPath, - }, - }, &options.ClientOptions) - if err != nil { - return nil, err - } - client := &VersionedClient{ - apiVersion: options.APIVersion, - endpoint: endpoint, - internal: cl, - } - return client, nil -} - -// NewVersionedSubGroupClient creates a new instance of [VersionedSubGroupClient]. -func (client *VersionedClient) NewVersionedSubGroupClient() *VersionedSubGroupClient { - return &VersionedSubGroupClient{ - apiVersion: client.apiVersion, - endpoint: client.endpoint, - internal: client.internal, - } -} -``` - -## The sub-client uses the propagated API version for its path - -```go versionedsubgroup_client -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. See License.txt in the project root for license information. -// Code generated by Microsoft (R) Go Code Generator. DO NOT EDIT. - -package testmodule - -import ( - "context" - "github.com/Azure/azure-sdk-for-go/sdk/azcore" - "github.com/Azure/azure-sdk-for-go/sdk/azcore/policy" - "github.com/Azure/azure-sdk-for-go/sdk/azcore/runtime" - "net/http" - "net/url" - "strings" -) - -// VersionedSubGroupClient contains the methods for the VersionedSubGroup group. -// Don't use this type directly, use [VersionedClient.NewVersionedSubGroupClient] instead. -// -// Generated from API version 2022-12-01-preview -type VersionedSubGroupClient struct { - internal *azcore.Client - apiVersion string - endpoint string -} - -// WithPathAPIVersion - -// If the operation fails it returns an *azcore.ResponseError type. -// - options - VersionedSubGroupClientWithPathAPIVersionOptions contains the optional parameters for the VersionedSubGroupClient.WithPathAPIVersion -// method. -func (client *VersionedSubGroupClient) WithPathAPIVersion(ctx context.Context, options *VersionedSubGroupClientWithPathAPIVersionOptions) (VersionedSubGroupClientWithPathAPIVersionResponse, error) { - var err error - req, err := client.withPathAPIVersionCreateRequest(ctx, options) - if err != nil { - return VersionedSubGroupClientWithPathAPIVersionResponse{}, err - } - httpResp, err := client.internal.Pipeline().Do(req) - if err != nil { - return VersionedSubGroupClientWithPathAPIVersionResponse{}, err - } - if !runtime.HasStatusCode(httpResp, http.StatusNoContent) { - err = runtime.NewResponseError(httpResp) - return VersionedSubGroupClientWithPathAPIVersionResponse{}, err - } - return VersionedSubGroupClientWithPathAPIVersionResponse{}, nil -} - -// withPathAPIVersionCreateRequest creates the WithPathAPIVersion request. -func (client *VersionedSubGroupClient) withPathAPIVersionCreateRequest(ctx context.Context, _ *VersionedSubGroupClientWithPathAPIVersionOptions) (*policy.Request, error) { - urlPath := "/sub/with-path-api-version/{apiVersion}" - apiVersion := version20221201Preview - if client.apiVersion != "" { - apiVersion = client.apiVersion - } - urlPath = strings.ReplaceAll(urlPath, "{apiVersion}", url.PathEscape(apiVersion)) - req, err := runtime.NewRequest(ctx, http.MethodHead, runtime.JoinPaths(client.endpoint, urlPath)) - if err != nil { - return nil, err - } - return req, nil -} -``` From 44a48ff05d49b7361c47c2e154c160c6fdd250bf Mon Sep 17 00:00:00 2001 From: tadelesh Date: Wed, 5 Aug 2026 17:57:13 +0800 Subject: [PATCH 6/6] refactor(typespec-go): remove redundant path version state Derive path API-version override support from existing client parameters, fully restore the adapter, codemodel, and versionedgroup test, and cover the override in the path-specific Spector test. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: a2fed377-7b26-45fc-b3aa-8b6d6df8b46a --- ...pi-version-override-2026-08-04-14-45-00.md | 2 +- .../src/codegen/core/operations.ts | 38 +++++++++++++---- packages/typespec-go/src/codemodel/client.ts | 4 -- .../typespec-go/src/tcgcadapter/clients.ts | 12 +----- .../apiversionpathgroup/path_client_test.go | 41 +++++++++++++++++++ .../versionedgroup/versioned_client_test.go | 32 --------------- 6 files changed, 73 insertions(+), 56 deletions(-) diff --git a/.chronus/changes/fix-go-path-api-version-override-2026-08-04-14-45-00.md b/.chronus/changes/fix-go-path-api-version-override-2026-08-04-14-45-00.md index 7aa5667bd6..21b50328e6 100644 --- a/.chronus/changes/fix-go-path-api-version-override-2026-08-04-14-45-00.md +++ b/.chronus/changes/fix-go-path-api-version-override-2026-08-04-14-45-00.md @@ -4,4 +4,4 @@ packages: - "@azure-tools/typespec-go" --- -Honor `ClientOptions.APIVersion` for API versions emitted in operation paths, including sub-client operations. +Honor `ClientOptions.APIVersion` for API versions emitted in operation paths. diff --git a/packages/typespec-go/src/codegen/core/operations.ts b/packages/typespec-go/src/codegen/core/operations.ts index f8fd88d1e8..802b371be3 100644 --- a/packages/typespec-go/src/codegen/core/operations.ts +++ b/packages/typespec-go/src/codegen/core/operations.ts @@ -20,6 +20,18 @@ export class OperationGroupContent { } } +function supportsPathAPIVersionOverride(client: go.Client): boolean { + return ( + client.instance?.kind === "constructable" && + client.parameters.some( + (param) => + param.kind === "pathScalarParam" && + param.isApiVersion && + go.isLiteralParameter(param.style), + ) + ); +} + /** * Creates the content for all the *_client.go files. * @@ -94,10 +106,11 @@ export function generateOperations( } const indent = new helpers.Indentation(); + const pathAPIVersionOverride = supportsPathAPIVersionOverride(client); clientText += `type ${client.name} struct {\n`; clientText += `${indent.get()}internal *${azureARM ? "arm" : "azcore"}.Client\n`; - if (client.hasPathAPIVersion) { + if (pathAPIVersionOverride) { clientText += `${indent.get()}apiVersion string\n`; } @@ -138,7 +151,13 @@ export function generateOperations( // end of client definition clientText += "}\n\n"; - clientText += generateConstructors(client, target, imports, indent); + clientText += generateConstructors( + client, + target, + imports, + indent, + pathAPIVersionOverride, + ); // generate client accessors and operations let opText = ""; @@ -149,9 +168,6 @@ export function generateOperations( opText += `func (client *${client.name}) ${clientAccessor.name}(${getAPIParametersSig(clientAccessor, imports)}) *${subClientDecl} {\n`; opText += `${indent.get()}return &${subClientDecl}{\n`; const initFields = new Array("internal: client.internal"); - if (clientAccessor.returns.hasPathAPIVersion) { - initFields.push("apiVersion: client.apiVersion"); - } // propagate all client params for (const param of clientAccessor.parameters) { // by convention, the client accessor params have the @@ -231,6 +247,7 @@ function generateConstructors( type: go.CodeModelType, imports: ImportManager, indent: helpers.Indentation, + pathAPIVersionOverride: boolean, ): string { if (client.instance?.kind !== "constructable") { return ""; @@ -420,7 +437,7 @@ function generateConstructors( // this is the ARM case imports.add("github.com/Azure/azure-sdk-for-go/sdk/azcore/arm"); prolog = ""; - if (client.hasPathAPIVersion) { + if (pathAPIVersionOverride) { prolog += emitDefaultOptions(go.getTypeDeclaration(clientOptions, client.pkg)); } prolog += `${indent.get()}cl, err := arm.NewClient(moduleName, moduleVersion, credential, options)\n`; @@ -501,7 +518,7 @@ function generateConstructors( // as any supplemental endpoint params are ephemeral and // consumed during client construction. indent.push(); - if (client.hasPathAPIVersion) { + if (pathAPIVersionOverride) { ctorText += `${indent.get()}apiVersion: options.APIVersion,\n`; } for (const parameter of client.parameters) { @@ -1122,7 +1139,6 @@ function createProtocolRequest( const methodParamGroups = helpers.getMethodParamGroups(method); const hasPathParams = methodParamGroups.pathParams.length > 0; - const pathAPIVersionOverride = method.receiver.type.hasPathAPIVersion; // storage needs the client.u to be the source-of-truth for the full path. // however, swagger requires that all operations specify a path, which is at odds with storage. @@ -1167,7 +1183,11 @@ function createProtocolRequest( if (pp.style === "literal") { // literals are always scalar types and require no empty checks paramValue = helpers.formatParamValue(pp, imports, indent); - if (pp.kind === "pathScalarParam" && pp.isApiVersion && pathAPIVersionOverride) { + if ( + pp.kind === "pathScalarParam" && + pp.isApiVersion && + supportsPathAPIVersionOverride(method.receiver.type) + ) { text += `${indent.get()}apiVersion := ${paramValue}\n`; text += `${indent.get()}if client.apiVersion != "" {\n`; text += `${indent.push().get()}apiVersion = client.apiVersion\n`; diff --git a/packages/typespec-go/src/codemodel/client.ts b/packages/typespec-go/src/codemodel/client.ts index b5c042f9de..5706195e9a 100644 --- a/packages/typespec-go/src/codemodel/client.ts +++ b/packages/typespec-go/src/codemodel/client.ts @@ -47,9 +47,6 @@ export interface Client { */ apiVersions: Array; - /** indicates that this client stores an API version override for operation paths */ - hasPathAPIVersion: boolean; - /** the parent client in a hierarchical client */ parent?: Client; } @@ -400,7 +397,6 @@ export class Client implements Client { this.parameters = new Array(); this.pkg = pkg; this.apiVersions = new Array(); - this.hasPathAPIVersion = false; } } diff --git a/packages/typespec-go/src/tcgcadapter/clients.ts b/packages/typespec-go/src/tcgcadapter/clients.ts index 2531b223c6..bc239aa0e2 100644 --- a/packages/typespec-go/src/tcgcadapter/clients.ts +++ b/packages/typespec-go/src/tcgcadapter/clients.ts @@ -1317,20 +1317,12 @@ export class ClientAdapter { | tcgc.SdkQueryParameter, ): go.MethodParameter { if (opParam.isApiVersionParam) { - // Header/query API versions are emitted inline and overridden by the pipeline. - // Path API version overrides must be stored on the client because the pipeline cannot - // replace a path segment after the request URL has been constructed. + // we emit the api version param inline as a literal, never as a param. + // the ClientOptions.APIVersion setting is used to change the version. let paramType: go.Literal | go.String; let paramStyle: go.ParameterStyle; if (opParam.clientDefaultValue) { const client = method.receiver.type; - if ( - opParam.kind === "path" && - opParam.onClient && - client.instance?.kind === "constructable" - ) { - client.hasPathAPIVersion = true; - } // check if we already have a ConstantDef for this API version. let versionConst = client.apiVersions.find( (e) => e.literal.literal === opParam.clientDefaultValue, diff --git a/packages/typespec-go/test/azure-http-specs/azure/client-generator-core/api-version/apiversionpathgroup/path_client_test.go b/packages/typespec-go/test/azure-http-specs/azure/client-generator-core/api-version/apiversionpathgroup/path_client_test.go index 002b36b790..87dac2857a 100644 --- a/packages/typespec-go/test/azure-http-specs/azure/client-generator-core/api-version/apiversionpathgroup/path_client_test.go +++ b/packages/typespec-go/test/azure-http-specs/azure/client-generator-core/api-version/apiversionpathgroup/path_client_test.go @@ -5,12 +5,28 @@ package apiversionpathgroup_test import ( "context" + "net/http" "testing" "apiversionpathgroup" + "github.com/Azure/azure-sdk-for-go/sdk/azcore" "github.com/stretchr/testify/require" ) +type captureTransport struct { + request *http.Request +} + +func (c *captureTransport) Do(request *http.Request) (*http.Response, error) { + c.request = request + return &http.Response{ + StatusCode: http.StatusOK, + Header: http.Header{}, + Body: http.NoBody, + Request: request, + }, nil +} + func TestPathClient_PathAPIVersion(t *testing.T) { client, err := apiversionpathgroup.NewPathClientWithNoCredential("http://localhost:3000", nil) require.NoError(t, err) @@ -19,3 +35,28 @@ func TestPathClient_PathAPIVersion(t *testing.T) { require.NoError(t, err) require.Zero(t, resp) } + +func TestPathClient_PathAPIVersionOverride(t *testing.T) { + const apiVersion = "2026-01-01-preview" + transport := &captureTransport{} + client, err := apiversionpathgroup.NewPathClientWithNoCredential( + "http://localhost:3000", + &apiversionpathgroup.PathClientOptions{ + ClientOptions: azcore.ClientOptions{ + APIVersion: apiVersion, + Transport: transport, + }, + }, + ) + require.NoError(t, err) + + resp, err := client.PathAPIVersion(context.Background(), nil) + require.NoError(t, err) + require.Zero(t, resp) + require.NotNil(t, transport.request) + require.Equal( + t, + "/azure/client-generator-core/api-version/path/"+apiVersion, + transport.request.URL.Path, + ) +} diff --git a/packages/typespec-go/test/http-specs/server/versions/versionedgroup/versioned_client_test.go b/packages/typespec-go/test/http-specs/server/versions/versionedgroup/versioned_client_test.go index 55524a8b56..63376c7f34 100644 --- a/packages/typespec-go/test/http-specs/server/versions/versionedgroup/versioned_client_test.go +++ b/packages/typespec-go/test/http-specs/server/versions/versionedgroup/versioned_client_test.go @@ -5,7 +5,6 @@ package versionedgroup_test import ( "context" - "net/http" "testing" "versionedgroup" @@ -13,20 +12,6 @@ import ( "github.com/stretchr/testify/require" ) -type captureTransport struct { - request *http.Request -} - -func (c *captureTransport) Do(request *http.Request) (*http.Response, error) { - c.request = request - return &http.Response{ - StatusCode: http.StatusOK, - Header: http.Header{}, - Body: http.NoBody, - Request: request, - }, nil -} - func TestVersionedClient_WithPathAPIVersion(t *testing.T) { client, err := versionedgroup.NewVersionedClientWithNoCredential("http://localhost:3000", nil) require.NoError(t, err) @@ -35,23 +20,6 @@ func TestVersionedClient_WithPathAPIVersion(t *testing.T) { require.True(t, resp.Success) } -func TestVersionedClient_WithPathAPIVersionOverride(t *testing.T) { - const apiVersion = "2023-01-01-preview" - transport := &captureTransport{} - client, err := versionedgroup.NewVersionedClientWithNoCredential("http://localhost:3000", &versionedgroup.VersionedClientOptions{ - ClientOptions: azcore.ClientOptions{ - APIVersion: apiVersion, - Transport: transport, - }, - }) - require.NoError(t, err) - - resp, err := client.WithPathAPIVersion(context.Background(), nil) - require.NoError(t, err) - require.True(t, resp.Success) - require.Equal(t, "/server/versions/versioned/with-path-api-version/"+apiVersion, transport.request.URL.Path) -} - func TestVersionedClient_WithQueryAPIVersion(t *testing.T) { client, err := versionedgroup.NewVersionedClientWithNoCredential("http://localhost:3000", nil) require.NoError(t, err)