Skip to content

Commit 8db91de

Browse files
authored
Wanl/fix fist preview version bump (#8671)
# Issue #8522 # Problem The bug is introduced by new codegen [pr](Azure/autorest.typescript@b0853b2). The way to define default api version is changed. extracting the api version should also consider version of codegen. e.g. new api-version at client level is defined [here](https://github.com/Azure/azure-sdk-for-js/blob/main/sdk/mongocluster/arm-mongocluster/src/rest/documentDBClient.ts#L23) while old one is: - [here](https://github.com/Azure/azure-sdk-for-js/blob/06716722818f5838cc10a9e5644b7ba9f32089d5/sdk/face/ai-vision-face-rest/src/faceClient.ts#L26) - [another](https://github.com/Azure/azure-sdk-for-js/blob/06716722818f5838cc10a9e5644b7ba9f32089d5/sdk/openai/openai-rest/src/openAIClient.ts#L22) # Solution 1. try to detect in new client, return api-version if found 2. fallback to detect old client, return whatever found
1 parent b32d70e commit 8db91de

15 files changed

Lines changed: 323 additions & 46 deletions
Lines changed: 97 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -1,81 +1,135 @@
1-
import { SourceFile, SyntaxKind } from "ts-morph";
2-
import shell from 'shelljs';
3-
import path from 'path';
1+
import { SourceFile, SyntaxKind } from "ts-morph";
2+
import shell from "shelljs";
3+
import path from "path";
4+
import * as ts from "typescript";
45

5-
import { ApiVersionType } from "../../common/types"
6+
import { ApiVersionType } from "../../common/types";
67
import { IApiVersionTypeExtractor } from "../../common/interfaces";
78
import { getTsSourceFile } from "../../common/utils";
9+
import { readFileSync } from "fs";
810

911
const findRestClientPath = (packageRoot: string): string => {
10-
const restPath = path.join(packageRoot, 'src/rest/');
12+
const restPath = path.join(packageRoot, "src/rest/");
1113
const fileNames = shell.ls(restPath);
12-
const clientFiles = fileNames.filter(f => f.endsWith("Client.ts"));
13-
if (clientFiles.length !== 1) throw new Error(`Single client is supported, but found ${clientFiles}`);
14+
const clientFiles = fileNames.filter((f) => f.endsWith("Client.ts"));
15+
if (clientFiles.length !== 1)
16+
throw new Error(`Single client is supported, but found "${clientFiles}" in ${restPath}`);
1417

1518
const clientPath = path.join(restPath, clientFiles[0]);
1619
return clientPath;
1720
};
1821

19-
const matchPattern = (text: string, pattern: RegExp): string | undefined => {
20-
const match = text.match(pattern);
21-
const found = match != null && match.length === 2;
22-
return found ? match?.at(1) : undefined;
23-
}
24-
25-
const findApiVersionInRestClient = (clientPath: string): string | undefined => {
22+
const findApiVersionInRestClientV1 = (
23+
clientPath: string
24+
): string | undefined => {
2625
const sourceFile = getTsSourceFile(clientPath);
2726
const createClientFunction = sourceFile?.getFunction("createClient");
28-
if (!createClientFunction) throw new Error("Function 'createClient' not found.");
27+
if (!createClientFunction)
28+
throw new Error("Function 'createClient' not found.");
2929

30-
const apiVersionStatements = createClientFunction.getStatements()
31-
.filter(s =>
32-
s.getKind() === SyntaxKind.ExpressionStatement &&
33-
s.getText().indexOf("options.apiVersion") > -1);
34-
if (apiVersionStatements.length === 0) return undefined;
30+
const apiVersionStatements = createClientFunction
31+
.getStatements()
32+
.filter((s) => s.getText().includes("options.apiVersion"));
33+
if (apiVersionStatements.length === 0) {
34+
return undefined;
35+
}
36+
const text =
37+
apiVersionStatements[apiVersionStatements.length - 1].getText();
38+
return extractApiVersionFromText(text);
39+
};
3540

36-
const text = apiVersionStatements[apiVersionStatements.length - 1].getText();
37-
const pattern = /(\d{4}-\d{2}-\d{2}(?:-preview)?)/;
38-
const apiVersion = matchPattern(text, pattern);
41+
const extractApiVersionFromText = (text: string): string | undefined => {
42+
const begin = text.indexOf('"');
43+
const end = text.lastIndexOf('"');
44+
return text.substring(begin + 1, end);
45+
};
46+
47+
// new ways in @autorest/typespec-ts emitter to set up api-version
48+
const findApiVersionInRestClientV2 = (clientPath: string): string | undefined => {
49+
const sourceCode= readFileSync(clientPath, {encoding: 'utf-8'})
50+
const sourceFile = ts.createSourceFile("example.ts", sourceCode, ts.ScriptTarget.Latest, true);
51+
const createClientFunction = sourceFile.statements.filter(s => (s as ts.FunctionDeclaration)?.name?.escapedText === 'createClient').map(s => (s as ts.FunctionDeclaration))[0];
52+
let apiVersion: string | undefined = undefined;
53+
createClientFunction.parameters.forEach(p => {
54+
const isBindingPattern = node => node && typeof node === "object" && "elements" in node && "parent" in node && "kind" in node;
55+
if (!isBindingPattern(p.name)) {
56+
return;
57+
}
58+
const binding = p.name as ts.ObjectBindingPattern;
59+
const apiVersionTexts = binding.elements?.filter(e => (e.name as ts.Identifier)?.escapedText === "apiVersion").map(e => e.initializer?.getText());
60+
// apiVersionTexts.length must be 0 or 1, otherwise the binding pattern contains the same keys, which causes a ts error
61+
if (apiVersionTexts.length === 1 && apiVersionTexts[0]) {
62+
apiVersion = extractApiVersionFromText(apiVersionTexts[0]);
63+
}
64+
});
3965
return apiVersion;
4066
};
4167

42-
const getApiVersionTypeFromRestClient: IApiVersionTypeExtractor = (packageRoot: string): ApiVersionType => {
68+
// workaround for createClient function changes it's way to setup api-version
69+
export const findApiVersionInRestClient = (clientPath: string): string | undefined => {
70+
const version2 = findApiVersionInRestClientV2(clientPath);
71+
if (version2) {
72+
return version2;
73+
}
74+
const version1 = findApiVersionInRestClientV1(clientPath);
75+
return version1;
76+
};
77+
78+
const getApiVersionTypeFromRestClient: IApiVersionTypeExtractor = (
79+
packageRoot: string
80+
): ApiVersionType => {
4381
const clientPath = findRestClientPath(packageRoot);
4482
const apiVersion = findApiVersionInRestClient(clientPath);
45-
if (apiVersion && apiVersion.indexOf("-preview") >= 0) return ApiVersionType.Preview;
46-
if (apiVersion && apiVersion.indexOf("-preview") < 0) return ApiVersionType.Stable;
83+
if (apiVersion && apiVersion.indexOf("-preview") >= 0)
84+
return ApiVersionType.Preview;
85+
if (apiVersion && apiVersion.indexOf("-preview") < 0)
86+
return ApiVersionType.Stable;
4787
return ApiVersionType.None;
4888
};
4989

50-
const findApiVersionsInOperations = (sourceFile: SourceFile | undefined): Array<string> | undefined => {
90+
const findApiVersionsInOperations = (
91+
sourceFile: SourceFile | undefined
92+
): Array<string> | undefined => {
5193
const interfaces = sourceFile?.getInterfaces();
52-
const interfacesWithApiVersion = interfaces?.filter(itf => itf.getProperty('"api-version"'));
53-
const apiVersions = interfacesWithApiVersion?.map(itf => {
54-
const property = itf.getMembers()
55-
.filter(m => {
56-
const defaultValue = m.getChildrenOfKind(SyntaxKind.StringLiteral)[0];
57-
return defaultValue && defaultValue.getText() === '"api-version"';
58-
})[0];
59-
const apiVersion = property.getChildrenOfKind(SyntaxKind.LiteralType)[0].getText();
94+
const interfacesWithApiVersion = interfaces?.filter((itf) =>
95+
itf.getProperty('"api-version"')
96+
);
97+
const apiVersions = interfacesWithApiVersion?.map((itf) => {
98+
const property = itf.getMembers().filter((m) => {
99+
const defaultValue = m.getChildrenOfKind(
100+
SyntaxKind.StringLiteral
101+
)[0];
102+
return defaultValue && defaultValue.getText() === '"api-version"';
103+
})[0];
104+
const apiVersion = property
105+
.getChildrenOfKind(SyntaxKind.LiteralType)[0]
106+
.getText();
60107
return apiVersion;
61108
});
62109
return apiVersions;
63-
}
110+
};
64111

65-
const getApiVersionTypeFromOperations: IApiVersionTypeExtractor = (packageRoot: string): ApiVersionType => {
66-
const paraPath = path.join(packageRoot, 'src/rest/parameters.ts');
112+
const getApiVersionTypeFromOperations: IApiVersionTypeExtractor = (
113+
packageRoot: string
114+
): ApiVersionType => {
115+
const paraPath = path.join(packageRoot, "src/rest/parameters.ts");
67116
const sourceFile = getTsSourceFile(paraPath);
68117
const apiVersions = findApiVersionsInOperations(sourceFile);
69118
if (!apiVersions) return ApiVersionType.None;
70-
const previewVersions = apiVersions.filter(v => v.indexOf("-preview") >= 0);
71-
return previewVersions.length > 0 ? ApiVersionType.Preview : ApiVersionType.Stable;
119+
const previewVersions = apiVersions.filter(
120+
(v) => v.indexOf("-preview") >= 0
121+
);
122+
return previewVersions.length > 0
123+
? ApiVersionType.Preview
124+
: ApiVersionType.Stable;
72125
};
73126

74-
// TODO: add unit test
75-
export const getApiVersionType: IApiVersionTypeExtractor = (packageRoot: string): ApiVersionType => {
127+
export const getApiVersionType: IApiVersionTypeExtractor = (
128+
packageRoot: string
129+
): ApiVersionType => {
76130
const typeFromClient = getApiVersionTypeFromRestClient(packageRoot);
77131
if (typeFromClient !== ApiVersionType.None) return typeFromClient;
78132
const typeFromOperations = getApiVersionTypeFromOperations(packageRoot);
79133
if (typeFromOperations !== ApiVersionType.None) return typeFromOperations;
80134
return ApiVersionType.Stable;
81-
}
135+
};
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
import { expect, test } from "vitest";
2+
import { findApiVersionInRestClient, getApiVersionType } from "../../mlc/apiVersion/apiVersionTypeExtractor";
3+
import { join } from "path";
4+
import { ApiVersionType } from "../../common/types";
5+
6+
test("MLC api-version Extractor: new createClient function", async () => {
7+
const clientPath = join(__dirname, 'testCases/new/src/rest/newClient.ts');
8+
const version = findApiVersionInRestClient(clientPath);
9+
expect(version).toBe('2024-03-01-preview');
10+
});
11+
12+
test("MLC api-version Extractor: get api version type from new createClient function", async () => {
13+
const root = join(__dirname, 'testCases/new/');
14+
const version = getApiVersionType(root);
15+
expect(version).toBe(ApiVersionType.Preview);
16+
});
17+
18+
test("MLC api-version Extractor: old createClient function 1", async () => {
19+
const clientPath = join(__dirname, 'testCases/old1/src/rest/oldClient.ts');
20+
const version = findApiVersionInRestClient(clientPath);
21+
expect(version).toBe('v1.1-preview.1');
22+
});
23+
24+
test("MLC api-version Extractor: get api version type from old createClient function 1", async () => {
25+
const root = join(__dirname, 'testCases/old1/');
26+
const version = getApiVersionType(root);
27+
expect(version).toBe(ApiVersionType.Preview);
28+
});
29+
30+
test("MLC api-version Extractor: old createClient function 2", async () => {
31+
const clientPath = join(__dirname, 'testCases/old2/src/rest/oldClient.ts');
32+
const version = findApiVersionInRestClient(clientPath);
33+
expect(version).toBe('2024-03-01');
34+
});
35+
36+
test("MLC api-version Extractor: get api version type from old createClient function 2", async () => {
37+
const root = join(__dirname, 'testCases/old2/');
38+
const version = getApiVersionType(root);
39+
expect(version).toBe(ApiVersionType.Stable);
40+
});
Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
// Copyright (c) Microsoft Corporation.
2+
// Licensed under the MIT license.
3+
4+
import { getClient, ClientOptions } from "@azure-rest/core-client";
5+
import { logger } from "../logger.js";
6+
import { TokenCredential } from "@azure/core-auth";
7+
import { DocumentDBContext } from "./clientDefinitions.js";
8+
9+
/** The optional parameters for the client */
10+
export interface DocumentDBContextOptions extends ClientOptions {
11+
/** The api version option of the client */
12+
apiVersion?: string;
13+
}
14+
15+
/**
16+
* Initialize a new instance of `DocumentDBContext`
17+
* @param credentials - uniquely identify client credential
18+
* @param options - the parameter for all optional parameters
19+
*/
20+
export default function createClient(
21+
credentials: TokenCredential,
22+
{
23+
apiVersion = "2024-03-01-preview",
24+
...options
25+
}: DocumentDBContextOptions = {},
26+
): DocumentDBContext {
27+
const endpointUrl =
28+
options.endpoint ?? options.baseUrl ?? `https://management.azure.com`;
29+
const userAgentInfo = `azsdk-js-arm-mongocluster/1.0.0-beta.1`;
30+
const userAgentPrefix =
31+
options.userAgentOptions && options.userAgentOptions.userAgentPrefix
32+
? `${options.userAgentOptions.userAgentPrefix} ${userAgentInfo}`
33+
: `${userAgentInfo}`;
34+
options = {
35+
...options,
36+
userAgentOptions: {
37+
userAgentPrefix,
38+
},
39+
loggingOptions: {
40+
logger: options.loggingOptions?.logger ?? logger.info,
41+
},
42+
credentials: {
43+
scopes: options.credentials?.scopes ?? [`${endpointUrl}/.default`],
44+
},
45+
};
46+
const client = getClient(
47+
endpointUrl,
48+
credentials,
49+
options,
50+
) as DocumentDBContext;
51+
52+
client.pipeline.removePolicy({ name: "ApiVersionPolicy" });
53+
client.pipeline.addPolicy({
54+
name: "ClientApiVersionPolicy",
55+
sendRequest: (req, next) => {
56+
// Use the apiVersion defined in request url directly
57+
// Append one if there is no apiVersion and we have one at client options
58+
const url = new URL(req.url);
59+
if (!url.searchParams.get("api-version") && apiVersion) {
60+
req.url = `${req.url}${Array.from(url.searchParams.keys()).length > 0 ? "&" : "?"
61+
}api-version=${apiVersion}`;
62+
}
63+
64+
return next(req);
65+
},
66+
});
67+
return client;
68+
}
Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
// Copyright (c) Microsoft Corporation.
2+
// Licensed under the MIT license.
3+
4+
import { getClient, ClientOptions } from "@azure-rest/core-client";
5+
import { logger } from "./logger.js";
6+
import { TokenCredential, KeyCredential } from "@azure/core-auth";
7+
import { FaceClient } from "./clientDefinitions.js";
8+
import { Versions } from "./models.js";
9+
10+
export interface FaceClientOptions extends ClientOptions {
11+
apiVersion?: Versions;
12+
}
13+
14+
/**
15+
* Initialize a new instance of `FaceClient`
16+
* @param endpointParam - Supported Cognitive Services endpoints (protocol and hostname, for example:
17+
* https://{resource-name}.cognitiveservices.azure.com).
18+
* @param credentials - uniquely identify client credential
19+
* @param options - the parameter for all optional parameters
20+
*/
21+
export default function createClient(
22+
endpointParam: string,
23+
credentials: TokenCredential | KeyCredential,
24+
options: FaceClientOptions = {},
25+
): FaceClient {
26+
const apiVersion = options.apiVersion ?? "v1.1-preview.1";
27+
const endpointUrl = options.endpoint ?? options.baseUrl ?? `${endpointParam}/face/${apiVersion}`;
28+
29+
const userAgentInfo = `azsdk-js-ai-vision-face-rest/1.0.0-beta.1`;
30+
const userAgentPrefix =
31+
options.userAgentOptions && options.userAgentOptions.userAgentPrefix
32+
? `${options.userAgentOptions.userAgentPrefix} ${userAgentInfo}`
33+
: `${userAgentInfo}`;
34+
options = {
35+
...options,
36+
userAgentOptions: {
37+
userAgentPrefix,
38+
},
39+
loggingOptions: {
40+
logger: options.loggingOptions?.logger ?? logger.info,
41+
},
42+
credentials: {
43+
scopes: options.credentials?.scopes ?? ["https://cognitiveservices.azure.com/.default"],
44+
apiKeyHeaderName: options.credentials?.apiKeyHeaderName ?? "Ocp-Apim-Subscription-Key",
45+
},
46+
};
47+
48+
const client = getClient(endpointUrl, credentials, options) as FaceClient;
49+
50+
client.pipeline.removePolicy({ name: "ApiVersionPolicy" });
51+
52+
client.pipeline.addPolicy({
53+
name: "VerifyImageFilenamePolicy",
54+
sendRequest: (request, next) => {
55+
for (const part of request.multipartBody?.parts ?? []) {
56+
const contentDisposition = part.headers.get("content-disposition");
57+
if (
58+
contentDisposition &&
59+
contentDisposition.includes(`name="VerifyImage"`) &&
60+
!contentDisposition.includes("filename=")
61+
) {
62+
part.headers.set("content-disposition", `form-data; name="VerifyImage"; filename="blob"`);
63+
}
64+
}
65+
return next(request);
66+
},
67+
});
68+
69+
return client;
70+
}

0 commit comments

Comments
 (0)