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

Filter by extension

Filter by extension

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

Honor `ClientOptions.APIVersion` for API versions emitted in operation paths.
12 changes: 12 additions & 0 deletions packages/typespec-go/src/codegen/core/helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,18 @@ export function canonicalizeHeaderName(name: string): string {
return canonicalName;
}

export function supportsPathAPIVersionOverride(client: go.Client): boolean {
return (
client.instance?.kind === "constructable" &&
client.parameters.some(
(param) =>
param.kind === "pathScalarParam" &&
param.isApiVersion &&
go.isLiteralParameter(param.style),
)
);
}

/**
* returns the parameter's type definition with a possible '*' prefix
*
Expand Down
34 changes: 28 additions & 6 deletions packages/typespec-go/src/codegen/core/operations.ts
Original file line number Diff line number Diff line change
Expand Up @@ -96,9 +96,13 @@ export function generateOperations(
}

const indent = new helpers.Indentation();
const pathAPIVersionOverride = helpers.supportsPathAPIVersionOverride(client);

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<go.ClientParameter>();
Expand Down Expand Up @@ -137,7 +141,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 = "";
Expand Down Expand Up @@ -228,6 +238,7 @@ function generateConstructors(
type: go.CodeModelType,
imports: ImportManager,
indent: helpers.Indentation,
pathAPIVersionOverride: boolean,
): string {
if (client.instance?.kind !== "constructable") {
return "";
Expand All @@ -236,6 +247,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
Expand Down Expand Up @@ -297,9 +314,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:
Expand All @@ -315,7 +330,7 @@ function generateConstructors(
case "queryScalarParam":
case "uriParam":
if (param.isApiVersion) {
apiVersionParam = param;
apiVersionParam ??= param;
}
}
}
Expand Down Expand Up @@ -412,7 +427,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 (pathAPIVersionOverride) {
prolog += emitDefaultOptions(go.getTypeDeclaration(clientOptions, client.pkg));
}
prolog += `${indent.get()}cl, err := arm.NewClient(moduleName, moduleVersion, credential, options)\n`;
break;
}
break;
Expand Down Expand Up @@ -490,6 +509,9 @@ function generateConstructors(
// as any supplemental endpoint params are ephemeral and
// consumed during client construction.
indent.push();
if (pathAPIVersionOverride) {
ctorText += `${indent.get()}apiVersion: options.APIVersion,\n`;
}
for (const parameter of client.parameters) {
if (go.isLiteralParameter(parameter.style)) {
continue;
Expand Down
11 changes: 11 additions & 0 deletions packages/typespec-go/src/codegen/core/request-handler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,17 @@ export function createRequestHandler(
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 &&
helpers.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`;
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).
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,59 @@
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) {
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)
}

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,
)
}
Original file line number Diff line number Diff line change
@@ -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"),
);
Original file line number Diff line number Diff line change
@@ -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"));
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
# 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) {
if options == nil {
options = &arm.ClientOptions{}
}
cl, err := arm.NewClient(moduleName, moduleVersion, credential, options)
if err != nil {
return nil, err
}
client := &TestClient{
apiVersion: options.APIVersion,
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) {
return TestClientGetResponse{}, runtime.NewResponseError(httpResp)
}
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
}
```
Loading
Loading