Skip to content

Commit 773aed1

Browse files
authored
Update RLC customization doc (Azure#21656)
* Update customization doc structure * Update docs * Update the customization doc to the latest * Update docs * Update folder
1 parent 87e8fdf commit 773aed1

File tree

1 file changed

+300
-0
lines changed

1 file changed

+300
-0
lines changed
Lines changed: 300 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,300 @@
1+
# Customization on the RLC rest-level client libraries
2+
3+
## Generate RLC Client
4+
5+
Follow [quickstart](https://aka.ms/azsdk/rlc/js) to generate the rest-level client from OpenAPI specs.
6+
7+
It's advised to put the generated code into the folder `generated`, add your customization code under the folder `src` and then export or re-export them as needed.
8+
9+
```yaml
10+
source-code-folder-path: ./src/generated
11+
```
12+
13+
## Custom authentication
14+
15+
Some services require a custom authentication flow. For example Metrics Advisor uses Key Authentication, however MA requires 2 headers for key authentication `Ocp-Apim-Subscription-Key` and `x-api-key`, which is different to the usual key authentication which only requires a single key.
16+
17+
In this case we customize as follows:
18+
19+
1. Hand author a `PipelinePolicy` that takes values for both keys and sign the request
20+
2. Hand author a wrapping client factory function
21+
3. In the wrapping factory, we create a new client with the generated factory
22+
4. Inject the new policy to the client
23+
5. Return the client
24+
6. Only expose the wrapping factory and hide the generated factory.
25+
26+
Here is the implementation in Metrics Advisor.
27+
28+
The wrapping function looks like:
29+
30+
```typescript
31+
import MetricsAdvisor from "./generated/generatedClient";
32+
import { isTokenCredential, TokenCredential } from "@azure/core-auth";
33+
import { ClientOptions } from "@azure-rest/core-client";
34+
import {
35+
createMetricsAdvisorKeyCredentialPolicy,
36+
MetricsAdvisorKeyCredential,
37+
} from "./metricsAdvisorKeyCredentialPolicy";
38+
39+
export default function createClient(
40+
endpoint: string,
41+
credential: TokenCredential | MetricsAdvisorKeyCredential,
42+
options: ClientOptions = {}
43+
): GeneratedClient {
44+
if (isTokenCredential(credential)) {
45+
return MetricsAdvisor(endpoint, credential, options);
46+
} else {
47+
const client = MetricsAdvisor(endpoint, undefined as any, options);
48+
const authPolicy = createMetricsAdvisorKeyCredentialPolicy(credential);
49+
client.pipeline.addPolicy(authPolicy);
50+
return client;
51+
}
52+
}
53+
```
54+
55+
And in `metricsAdvisorKeyCredentialPolicy.ts` file we have the customized policy and `createMetricsAdvisorKeyCredentialPolicy` function to create that policy
56+
57+
```typescript
58+
import {
59+
PipelinePolicy,
60+
PipelineRequest,
61+
PipelineResponse,
62+
SendRequest,
63+
} from "@azure/core-rest-pipeline";
64+
import { KeyCredential } from "@azure/core-auth";
65+
export const API_KEY_HEADER_NAME = "Ocp-Apim-Subscription-Key";
66+
export const X_API_KEY_HEADER_NAME = "x-api-key";
67+
68+
/**
69+
* Interface parameters for updateKey function
70+
*/
71+
export interface MetricsAdvisorKeyCredential extends KeyCredential {
72+
/** API key from the Metrics Advisor web portal */
73+
// key?: string; // extended from KeyCredential
74+
/** Subscription access key from the Azure portal */
75+
subscriptionKey?: string;
76+
}
77+
78+
/**
79+
* Creates an HTTP pipeline policy to authenticate a request
80+
* using an `MetricsAdvisorKeyCredential`
81+
*/
82+
export function createMetricsAdvisorKeyCredentialPolicy(
83+
credential: MetricsAdvisorKeyCredential
84+
): PipelinePolicy {
85+
return {
86+
name: "metricsAdvisorKeyCredentialPolicy",
87+
sendRequest(request: PipelineRequest, next: SendRequest): Promise<PipelineResponse> {
88+
if (!request) {
89+
throw new Error("webResource cannot be null or undefined");
90+
}
91+
request.headers.set(API_KEY_HEADER_NAME, credential.subscriptionKey || "");
92+
request.headers.set(X_API_KEY_HEADER_NAME, credential.key);
93+
return next(request);
94+
},
95+
};
96+
}
97+
```
98+
99+
With this user experience is the same as it is with any other RLC, as they just need to create a new client from the default exported factory function.
100+
101+
```typescript
102+
import MetricsAdvisor, { paginate } from "@azure-rest/ai-metricsadvisor";
103+
104+
const client = MetricsAdvisor("https://<endopoint>", {
105+
key: "<apiKey>",
106+
subscriptionKey: "<subscriptionKey>",
107+
});
108+
```
109+
110+
## Custom paging helper
111+
112+
Eventhough the code generator provides a pagination helper for RLCs, there are services that implement their own pagination pattern, different to the standard specification of `x-ms-pageable`.
113+
114+
One example is the Metrics Advisor service, which implements a pagination pattern in which getting the next page can be called with `GET` or `POST` depending on the resource.
115+
116+
The standard pagination pattern, assumes `GET` for getting the next pages. In this case we implemented a custom paginate helper that has the same public interface as the generated helper but under the hoods has an additional pagination implementation to use `POST`. Also this custom helper has an internal map that indicates which operations need `POST` and which need `GET`.
117+
118+
Here is the implementation in Metrics Advisor and remember to replace the `paginationMapping` as yours. The generated paging helper is hidden and the custom paginate helper is exposed.
119+
120+
```typescript
121+
import { getPagedAsyncIterator, PagedAsyncIterableIterator, PagedResult } from "@azure/core-paging";
122+
import { Client, createRestError, PathUncheckedResponse } from "@azure-rest/core-client";
123+
import { PaginateReturn, PagingOptions } from "./generated/paginateHelper";
124+
125+
export function paginate<TResponse extends PathUncheckedResponse>(
126+
client: Client,
127+
initialResponse: TResponse,
128+
options: PagingOptions<TResponse> = {}
129+
): PagedAsyncIterableIterator<PaginateReturn<TResponse>> {
130+
// internal map to indicate which operation uses which method
131+
const paginationMapping: Record<string, any> = {
132+
"/feedback/metric/query": {
133+
method: "POST",
134+
},
135+
"/dataFeeds": {
136+
method: "GET",
137+
},
138+
"/hooks": {
139+
method: "GET",
140+
},
141+
};
142+
143+
// Extract element type from initial response
144+
type TElement = PaginateReturn<TResponse>;
145+
let firstRun = true;
146+
// We need to check the response for success before trying to inspect it looking for
147+
// the properties to use for nextLink and itemName
148+
checkPagingRequest(initialResponse);
149+
const { itemName, nextLinkName } = getPaginationProperties(initialResponse);
150+
const { customGetPage } = options;
151+
const pagedResult: PagedResult<TElement[]> = {
152+
firstPageLink: "",
153+
getPage:
154+
typeof customGetPage === "function"
155+
? customGetPage
156+
: async (pageLink: string) => {
157+
// Calculate using get or post
158+
let result;
159+
if (paginationMapping[initialResponse.request.url]?.method == "POST") {
160+
result = firstRun
161+
? initialResponse
162+
: await client.pathUnchecked(pageLink).post({ body: initialResponse.request.body });
163+
} else {
164+
result = firstRun ? initialResponse : await client.pathUnchecked(pageLink).get();
165+
}
166+
firstRun = false;
167+
checkPagingRequest(result);
168+
const nextLink = getNextLink(result.body, nextLinkName);
169+
const values = getElements<TElement>(result.body, itemName);
170+
return {
171+
page: values,
172+
nextPageLink: nextLink,
173+
};
174+
},
175+
};
176+
177+
return getPagedAsyncIterator(pagedResult);
178+
}
179+
```
180+
181+
The example code to call the helper.
182+
183+
```typescript
184+
import MetricsAdvisor, { paginate } from "@azure-rest/ai-metricsadvisor";
185+
import { DefaultAzureCredential } from "@azure/identity";
186+
187+
const client = MetricsAdvisor("https://<endopoint>", new DefaultAzureCredential());
188+
189+
const initResponse = await client.listDataFeeds({
190+
queryParameters: {
191+
dataFeedName: "js-test-",
192+
$skip: 1,
193+
$maxpagesize: 1,
194+
},
195+
});
196+
197+
const dataFeeds = paginate(client, initResponse);
198+
for await (const dataFeed of dataFeeds) {
199+
console.log(data);
200+
}
201+
```
202+
203+
## Custom data transform helpers
204+
205+
There may be times in which transforming the data from the service would be beneficial. When a transformation is common for our customers we may decide to expose helper transformation functions. These helper transformations are optional and customers can decide to use them or not, the calls maintain the original data form from the Service.
206+
207+
If we export `toDataFeedDetailResponse` which may convert the REST model to a common one, so that the customers could call this way:
208+
209+
```typescript
210+
import MetricsAdvisor, { toDataFeedDetailResponse } from "@azure-rest/ai-metricsadvisor";
211+
import { DefaultAzureCredential } from "@azure/identity";
212+
213+
const client = MetricsAdvisor("https://<endpoint>", new DefaultAzureCredential());
214+
const listResponse = await client.listDataFeeds(<parameter>);
215+
if (listResponse.status != "201") {
216+
throw new Error("Error");
217+
}
218+
219+
// Transforms service data into a more useful shape
220+
const formattedDatafeed = toDataFeedDetailResponse(listResponse);
221+
```
222+
223+
## Multi-client packages
224+
225+
There are cases where 2 services are closely related that most users will need to use both in the same application, in this case, we may opt for multi-client packages. Each client can be imported individually without a top-level client, this is to work nicely with bundler TreeShaking.
226+
227+
We could leverage the autorest batch option and enable multi-client flag in our `README.md` to generate two or more service clients.
228+
229+
Here is an example in metrics advisor, we have two clients `MetricsAdvisorClient` and `MetricsAdvisorAdministrationClient`.
230+
231+
### Use multi-client flag and batch option
232+
233+
Add the `multi-client` flag in our readme and use the `batch` autorest option to create the two clients:
234+
235+
```yaml $(multi-client)
236+
batch:
237+
- metrics-advisor: true
238+
- metrics-advisor-admin: true
239+
```
240+
241+
### Specify configurations for each individual clients
242+
243+
For each individual clients specify your client name and swagger file. Make sure that you don't have one Swagger with operations that are designed to be in two different clients so that clients should correspond to a clear set of Swagger files.
244+
245+
Normally, the folder structure would be something like `sdk/{servicename}/{servicename}-{modulename}-rest`. For example, we have `sdk/agrifood/agrifood-farming-rest` folder for Farmbeats account modules. That folder will be your **${PROJECT_ROOT} folder**.
246+
247+
```yaml $(metrics-advisor) == true
248+
title: MetricsAdvisorClient
249+
description: Metrics Advisor Client
250+
output-folder: ${PROJECT_ROOT}/src
251+
source-code-folder-path: ./client
252+
input-file: /your/swagger/folder/metricsadvisor.json
253+
```
254+
255+
```yaml $(metrics-advisor-admin) == true
256+
title: MetricsAdvisorAdministrationClient
257+
description: Metrics Advisor Admin Client
258+
output-folder: ${PROJECT_ROOT}/src
259+
source-code-folder-path: ./admin
260+
input-file: /your/swagger/folder/metricsadvisor-admin.json
261+
```
262+
263+
### Generate code with `--multi-client`
264+
265+
When generating the code specify that what we want is multi-client so append the flag in command line `--multi-client`. After generation the folder structure would be like below:
266+
267+
```
268+
${PROJECT_ROOT}/
269+
├─ src/
270+
│ ├─ client/
271+
│ │ ├─ MetricsAdvisorClient.ts
272+
│ │ ├─ index.ts
273+
│ ├─ admin/
274+
│ │ ├─ MetricsAdvisorAdministrationClient.ts
275+
│ │ ├─ index.ts
276+
│ ├─ index.ts
277+
```
278+
279+
### Example code to call any client
280+
281+
```typescript
282+
import {
283+
MetricsAdvisorAdministrationClient,
284+
MetricsAdvisorClient,
285+
} from "@azure-rest/ai-metrics-advisor";
286+
const adminClient = MetricsAdvisorAdministrationClient.createClient(endpoint, credential);
287+
// call any admin operation
288+
const createdResponse = await adminClient.createDataFeed(`<parameter>`);
289+
const maClient = MetricsAdvisorClient.createClient(endpoint, credential);
290+
// call any non-admin operation
291+
const listedResponse = await maClient.getIncidentsByAnomalyDetectionConfiguration(`<parameter>`);
292+
```
293+
294+
## RLC Customization Considerations
295+
296+
Our customization strategy has the following principles:
297+
298+
- Expose custom functionality as helper functions that users can opt-in
299+
- Never force customers to use a customized function or operation
300+
- The only exception is if we need to add custom policies to the client, it is okay to wrap the generated client factory and exposed the wrapped factory instead of the generated one.

0 commit comments

Comments
 (0)