@azure-tools/typespec-ts 0.50.2 → 0.50.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (66) hide show
  1. package/CHANGELOG.md +6 -0
  2. package/dist/src/framework/hooks/binder.d.ts +1 -1
  3. package/dist/src/framework/hooks/binder.d.ts.map +1 -1
  4. package/dist/src/framework/hooks/binder.js +11 -3
  5. package/dist/src/framework/hooks/binder.js.map +1 -1
  6. package/dist/src/framework/load-static-helpers.d.ts +3 -0
  7. package/dist/src/framework/load-static-helpers.d.ts.map +1 -1
  8. package/dist/src/framework/load-static-helpers.js +49 -38
  9. package/dist/src/framework/load-static-helpers.js.map +1 -1
  10. package/dist/src/index.d.ts.map +1 -1
  11. package/dist/src/index.js +19 -10
  12. package/dist/src/index.js.map +1 -1
  13. package/dist/src/lib.d.ts +7 -0
  14. package/dist/src/lib.d.ts.map +1 -1
  15. package/dist/src/lib.js +5 -0
  16. package/dist/src/lib.js.map +1 -1
  17. package/dist/src/modular/buildOperations.d.ts.map +1 -1
  18. package/dist/src/modular/buildOperations.js +1 -1
  19. package/dist/src/modular/buildOperations.js.map +1 -1
  20. package/dist/src/modular/emitModels.d.ts +8 -0
  21. package/dist/src/modular/emitModels.d.ts.map +1 -1
  22. package/dist/src/modular/emitModels.js +32 -2
  23. package/dist/src/modular/emitModels.js.map +1 -1
  24. package/dist/src/modular/emitSamples.js +9 -4
  25. package/dist/src/modular/emitSamples.js.map +1 -1
  26. package/dist/src/modular/emitTests.d.ts +7 -0
  27. package/dist/src/modular/emitTests.d.ts.map +1 -0
  28. package/dist/src/modular/emitTests.js +160 -0
  29. package/dist/src/modular/emitTests.js.map +1 -0
  30. package/dist/src/modular/external-dependencies.d.ts +42 -0
  31. package/dist/src/modular/external-dependencies.d.ts.map +1 -1
  32. package/dist/src/modular/external-dependencies.js +42 -0
  33. package/dist/src/modular/external-dependencies.js.map +1 -1
  34. package/dist/src/modular/helpers/exampleValueHelpers.d.ts +83 -0
  35. package/dist/src/modular/helpers/exampleValueHelpers.d.ts.map +1 -0
  36. package/dist/src/modular/helpers/exampleValueHelpers.js +631 -0
  37. package/dist/src/modular/helpers/exampleValueHelpers.js.map +1 -0
  38. package/dist/src/modular/helpers/operationHelpers.d.ts +22 -2
  39. package/dist/src/modular/helpers/operationHelpers.d.ts.map +1 -1
  40. package/dist/src/modular/helpers/operationHelpers.js +178 -9
  41. package/dist/src/modular/helpers/operationHelpers.js.map +1 -1
  42. package/dist/src/modular/static-helpers-metadata.d.ts +12 -0
  43. package/dist/src/modular/static-helpers-metadata.d.ts.map +1 -1
  44. package/dist/src/modular/static-helpers-metadata.js +12 -0
  45. package/dist/src/modular/static-helpers-metadata.js.map +1 -1
  46. package/dist/src/transform/transfromRLCOptions.d.ts.map +1 -1
  47. package/dist/src/transform/transfromRLCOptions.js +10 -0
  48. package/dist/src/transform/transfromRLCOptions.js.map +1 -1
  49. package/dist/tsconfig.tsbuildinfo +1 -1
  50. package/package.json +2 -2
  51. package/src/framework/hooks/binder.ts +15 -5
  52. package/src/framework/load-static-helpers.ts +79 -51
  53. package/src/index.ts +22 -7
  54. package/src/lib.ts +13 -0
  55. package/src/modular/buildOperations.ts +2 -1
  56. package/src/modular/emitModels.ts +47 -2
  57. package/src/modular/emitSamples.ts +7 -1
  58. package/src/modular/emitTests.ts +227 -0
  59. package/src/modular/external-dependencies.ts +43 -0
  60. package/src/modular/helpers/exampleValueHelpers.ts +940 -0
  61. package/src/modular/helpers/operationHelpers.ts +229 -17
  62. package/src/modular/static-helpers-metadata.ts +13 -0
  63. package/src/transform/transfromRLCOptions.ts +14 -0
  64. package/static/static-helpers/serialization/get-binary-response-body-browser.mts +22 -0
  65. package/static/static-helpers/serialization/get-binary-response-body.ts +24 -0
  66. package/static/test-helpers/recordedClient.ts +30 -0
@@ -0,0 +1,940 @@
1
+ import {
2
+ SdkHttpOperationExample,
3
+ SdkHttpParameterExampleValue,
4
+ SdkExampleValue,
5
+ SdkClientInitializationType,
6
+ SdkClientType,
7
+ SdkServiceOperation,
8
+ SdkModelPropertyType,
9
+ isReadOnly
10
+ } from "@azure-tools/typespec-client-generator-core";
11
+ import {
12
+ isAzurePackage,
13
+ NameType,
14
+ normalizeName
15
+ } from "@azure-tools/rlc-common";
16
+ import { resolveReference } from "../../framework/reference.js";
17
+ import { SdkContext } from "../../utils/interfaces.js";
18
+ import {
19
+ AzureIdentityDependencies,
20
+ AzureTestDependencies
21
+ } from "../external-dependencies.js";
22
+ import {
23
+ hasKeyCredential,
24
+ hasTokenCredential
25
+ } from "../../utils/credentialUtils.js";
26
+ import { isSpreadBodyParameter } from "./typeHelpers.js";
27
+ import { getClassicalClientName } from "./namingHelpers.js";
28
+ import {
29
+ getMethodHierarchiesMap,
30
+ ServiceOperation
31
+ } from "../../utils/operationUtil.js";
32
+ import { getSubscriptionId } from "../../transform/transfromRLCOptions.js";
33
+ import { SourceFile } from "ts-morph";
34
+ import { useContext } from "../../contextManager.js";
35
+ import { join } from "path";
36
+ import { getOperationFunction } from "./operationHelpers.js";
37
+ import { getClientParametersDeclaration } from "./clientHelpers.js";
38
+
39
+ /**
40
+ * Common interfaces for both samples and tests
41
+ */
42
+ export interface CommonValue {
43
+ name: string;
44
+ value: string;
45
+ isOptional: boolean;
46
+ onClient: boolean;
47
+ }
48
+
49
+ export interface ClientEmitOptions {
50
+ client: SdkClientType<SdkServiceOperation>;
51
+ generatedFiles: SourceFile[];
52
+ classicalMethodPrefix?: string;
53
+ subFolder?: string;
54
+ hierarchies?: string[]; // Add hierarchies to track operation path
55
+ }
56
+
57
+ /**
58
+ * Build parameter value map from example
59
+ */
60
+ export function buildParameterValueMap(
61
+ example: SdkHttpOperationExample
62
+ ): Record<string, SdkHttpParameterExampleValue> {
63
+ const parameterMap: Record<string, SdkHttpParameterExampleValue> = {};
64
+ example.parameters.forEach(
65
+ (param) => (parameterMap[param.parameter.serializedName] = param)
66
+ );
67
+ return parameterMap;
68
+ }
69
+
70
+ /**
71
+ * Prepare a common value for tests
72
+ */
73
+ export function prepareCommonValue(
74
+ name: string,
75
+ value: SdkExampleValue | string,
76
+ isOptional?: boolean,
77
+ onClient?: boolean
78
+ ): CommonValue {
79
+ return {
80
+ name: normalizeName(name, NameType.Parameter),
81
+ value: typeof value === "string" ? value : serializeExampleValue(value),
82
+ isOptional: Boolean(isOptional),
83
+ onClient: Boolean(onClient)
84
+ };
85
+ }
86
+
87
+ /**
88
+ * Get credential value for samples
89
+ */
90
+ export function getCredentialSampleValue(
91
+ dpgContext: SdkContext,
92
+ initialization: SdkClientInitializationType
93
+ ): CommonValue | undefined {
94
+ const keyCredential = hasKeyCredential(initialization),
95
+ tokenCredential = hasTokenCredential(initialization);
96
+ const defaultSetting = {
97
+ isOptional: false,
98
+ onClient: true,
99
+ name: "credential"
100
+ };
101
+ if (keyCredential || tokenCredential) {
102
+ if (isAzurePackage({ options: dpgContext.rlcOptions })) {
103
+ // Support DefaultAzureCredential for Azure packages
104
+ return {
105
+ ...defaultSetting,
106
+ value: `new ${resolveReference(
107
+ AzureIdentityDependencies.DefaultAzureCredential
108
+ )}()`
109
+ };
110
+ } else if (keyCredential) {
111
+ // Support ApiKeyCredential for non-Azure packages
112
+ return {
113
+ ...defaultSetting,
114
+ value: `{ key: "INPUT_YOUR_KEY_HERE" }`
115
+ };
116
+ } else if (tokenCredential) {
117
+ // Support TokenCredential for non-Azure packages
118
+ return {
119
+ ...defaultSetting,
120
+ value: `{ getToken: async () => {
121
+ return { token: "INPUT_YOUR_TOKEN_HERE", expiresOnTimestamp: Date.now() }; } }`
122
+ };
123
+ }
124
+ }
125
+ return undefined;
126
+ }
127
+
128
+ /**
129
+ * Get credential value for tests
130
+ */
131
+ export function getCredentialTestValue(
132
+ dpgContext: SdkContext,
133
+ initialization: SdkClientInitializationType
134
+ ): CommonValue | undefined {
135
+ const createTestCredentialType = resolveReference(
136
+ AzureTestDependencies.createTestCredential
137
+ );
138
+ const keyCredential = hasKeyCredential(initialization),
139
+ tokenCredential = hasTokenCredential(initialization);
140
+ const defaultSetting = {
141
+ isOptional: false,
142
+ onClient: true,
143
+ name: "credential"
144
+ };
145
+
146
+ if (keyCredential || tokenCredential) {
147
+ if (dpgContext.arm || hasTokenCredential(initialization)) {
148
+ // Support createTestCredential for ARM/Azure packages
149
+ return {
150
+ ...defaultSetting,
151
+ value: `${createTestCredentialType}()`
152
+ };
153
+ } else if (keyCredential) {
154
+ // Support ApiKeyCredential for non-Azure packages
155
+ return {
156
+ ...defaultSetting,
157
+ value: `{ key: "INPUT_YOUR_KEY_HERE" } `
158
+ };
159
+ } else if (tokenCredential) {
160
+ // Support TokenCredential for non-Azure packages
161
+ return {
162
+ ...defaultSetting,
163
+ value: `{
164
+ getToken: async () => {
165
+ return { token: "INPUT_YOUR_TOKEN_HERE", expiresOnTimestamp: Date.now() };
166
+ }
167
+ } `
168
+ };
169
+ }
170
+ }
171
+ return undefined;
172
+ }
173
+
174
+ /**
175
+ * Serialize example value to string representation for tests.
176
+ * Note: This is a simplified serializer for tests that does NOT handle plainDate
177
+ * as Date objects (it stays as string), which is the expected behavior for tests.
178
+ */
179
+ export function serializeExampleValue(value: SdkExampleValue): string {
180
+ let retValue = `{} as any`;
181
+ switch (value.kind) {
182
+ case "string": {
183
+ switch (value.type.kind) {
184
+ case "utcDateTime":
185
+ retValue = `new Date("${value.value}")`;
186
+ break;
187
+ case "bytes": {
188
+ const encode = value.type.encode ?? "base64";
189
+ // TODO: add check for un-supported encode
190
+ retValue = `Buffer.from("${value.value}", "${encode}")`;
191
+ break;
192
+ }
193
+ default:
194
+ retValue = `"${value.value
195
+ ?.toString()
196
+ .replace(/\\/g, "\\\\")
197
+ .replace(/"/g, '\\"')
198
+ .replace(/\n/g, "\\n")
199
+ .replace(/\r/g, "\\r")
200
+ .replace(/\t/g, "\\t")
201
+ .replace(/\f/g, "\\f")
202
+ .replace(/>/g, ">")
203
+ .replace(/</g, "<")}"`;
204
+ break;
205
+ }
206
+ break;
207
+ }
208
+ case "boolean":
209
+ case "number":
210
+ case "null":
211
+ case "unknown":
212
+ case "union":
213
+ retValue = `${JSON.stringify(value.value)}`;
214
+ break;
215
+ case "dict":
216
+ case "model": {
217
+ const mapper = buildTestPropertyNameMapper(value.type);
218
+ const values = [];
219
+ const additionalPropertiesValue =
220
+ value.kind === "model" ? (value.additionalPropertiesValue ?? {}) : {};
221
+ for (const propName in {
222
+ ...value.value
223
+ }) {
224
+ let property;
225
+ if (value.type.kind === "model") {
226
+ property = value.type.properties.find((p) => p.name === propName);
227
+ }
228
+ const propValue = value.value[propName];
229
+ if (propValue === undefined || propValue === null) {
230
+ continue;
231
+ }
232
+ // Skip readonly properties as they cannot be set by users
233
+ if (property && isReadOnly(property as SdkModelPropertyType)) {
234
+ continue;
235
+ }
236
+ // Handle flattened properties: inline inner model properties at current level
237
+ if (
238
+ property?.flatten &&
239
+ property.type.kind === "model" &&
240
+ propValue.kind === "model"
241
+ ) {
242
+ const innerMapper = buildTestPropertyNameMapper(property.type);
243
+ for (const innerPropName in propValue.value ?? {}) {
244
+ const innerPropValue = propValue.value[innerPropName];
245
+ if (innerPropValue === undefined || innerPropValue === null) {
246
+ continue;
247
+ }
248
+ const innerProperty = property.type.properties.find(
249
+ (p) => p.name === innerPropName
250
+ );
251
+ if (
252
+ innerProperty &&
253
+ isReadOnly(innerProperty as SdkModelPropertyType)
254
+ ) {
255
+ continue;
256
+ }
257
+ values.push(
258
+ `"${innerMapper.get(innerPropName) ?? innerPropName}": ` +
259
+ serializeExampleValue(innerPropValue)
260
+ );
261
+ }
262
+ continue;
263
+ }
264
+ const propRetValue =
265
+ `"${mapper.get(propName) ?? propName}": ` +
266
+ serializeExampleValue(propValue);
267
+ values.push(propRetValue);
268
+ }
269
+ const additionalBags = [];
270
+ for (const propName in {
271
+ ...additionalPropertiesValue
272
+ }) {
273
+ const propValue = additionalPropertiesValue[propName];
274
+ if (propValue === undefined || propValue === null) {
275
+ continue;
276
+ }
277
+ const propRetValue =
278
+ `"${mapper.get(propName) ?? propName}": ` +
279
+ serializeExampleValue(propValue);
280
+ additionalBags.push(propRetValue);
281
+ }
282
+ if (additionalBags.length > 0) {
283
+ const name = mapper.get("additionalProperties")
284
+ ? "additionalPropertiesBag"
285
+ : "additionalProperties";
286
+ values.push(`"${name}": {
287
+ ${additionalBags.join(", ")}
288
+ }`);
289
+ }
290
+
291
+ retValue = `{${values.join(", ")}}`;
292
+ break;
293
+ }
294
+ case "array": {
295
+ const valuesArr = value.value.map(serializeExampleValue);
296
+ retValue = `[${valuesArr.join(", ")}]`;
297
+ break;
298
+ }
299
+ default:
300
+ break;
301
+ }
302
+ return retValue;
303
+ }
304
+
305
+ /**
306
+ * Build a simple property name mapper for tests (does not require context).
307
+ * Maps serialized property names to their TypeScript/JavaScript client names.
308
+ */
309
+ function buildTestPropertyNameMapper(type: SdkExampleValue["type"]) {
310
+ const mapper = new Map<string, string>();
311
+ if (!type || type.kind !== "model") {
312
+ return mapper;
313
+ }
314
+ for (const prop of type.properties) {
315
+ if (prop.kind !== "property") {
316
+ continue;
317
+ }
318
+ mapper.set(
319
+ prop.serializationOptions.json?.name || prop.name,
320
+ normalizeName(prop.name, NameType.Property)
321
+ );
322
+ }
323
+ return mapper;
324
+ }
325
+
326
+ /**
327
+ * Escape special characters to spaces (for samples)
328
+ */
329
+ export function escapeSpecialCharToSpace(str: string): string {
330
+ if (!str) {
331
+ return str;
332
+ }
333
+ return str.replace(/_|,|\.|\(|\)|'s |\[|\]/g, " ").replace(/\//g, " Or ");
334
+ }
335
+
336
+ /**
337
+ * Generate descriptive names based on operation names
338
+ */
339
+ export function getDescriptiveName(
340
+ method: { doc?: string; oriName?: string; name: string },
341
+ exampleName: string,
342
+ type: "sample" | "test"
343
+ ): string {
344
+ const description = method.doc ?? `execute ${method.oriName ?? method.name}`;
345
+ let descriptiveName =
346
+ description.charAt(0).toLowerCase() + description.slice(1);
347
+
348
+ // Only remove trailing dots for test names to avoid redundancy
349
+ if (type === "test") {
350
+ descriptiveName = descriptiveName.replace(/\.$/, "");
351
+ // Include the example name to ensure uniqueness for multiple test cases
352
+ const functionName = normalizeName(exampleName, NameType.Method);
353
+ return `${descriptiveName} for ${functionName}`;
354
+ } else {
355
+ // For samples, preserve the original formatting including periods
356
+ return descriptiveName;
357
+ }
358
+ }
359
+
360
+ /**
361
+ * Common logic for preparing parameters for tests
362
+ */
363
+ export function prepareCommonParameters(
364
+ dpgContext: SdkContext,
365
+ method: ServiceOperation,
366
+ parameterMap: Record<string, SdkHttpParameterExampleValue>,
367
+ topLevelClient: SdkClientType<SdkServiceOperation>
368
+ ): CommonValue[] {
369
+ const envType = resolveReference(AzureTestDependencies.env);
370
+ const result: CommonValue[] = [];
371
+
372
+ const clientParams = getClientParametersDeclaration(
373
+ topLevelClient,
374
+ dpgContext,
375
+ {
376
+ onClientOnly: true
377
+ }
378
+ );
379
+
380
+ for (const param of clientParams) {
381
+ if (param.name === "options" || param.name === "credential") {
382
+ continue;
383
+ }
384
+
385
+ const exampleValue: CommonValue = {
386
+ name: param.name === "endpointParam" ? "endpoint" : param.name,
387
+ value: getEnvironmentVariableName(
388
+ param.name,
389
+ getClassicalClientName(topLevelClient)
390
+ ),
391
+ isOptional: Boolean(param.hasQuestionToken),
392
+ onClient: true
393
+ };
394
+
395
+ result.push(exampleValue);
396
+ }
397
+
398
+ // Handle credentials for tests
399
+ const credentialValue = getCredentialTestValue(
400
+ dpgContext,
401
+ topLevelClient.clientInitialization
402
+ );
403
+ if (credentialValue) {
404
+ result.push(credentialValue);
405
+ }
406
+
407
+ let subscriptionIdValue = `${envType}.SUBSCRIPTION_ID || "<SUBSCRIPTION_ID>"`;
408
+ let isSubscriptionIdAdded = false;
409
+
410
+ // Process required parameters
411
+ for (const param of method.operation.parameters) {
412
+ if (
413
+ param.optional === true ||
414
+ param.type.kind === "constant" ||
415
+ param.clientDefaultValue
416
+ ) {
417
+ continue;
418
+ }
419
+
420
+ const exampleValue = parameterMap[param.serializedName];
421
+
422
+ // Handle subscriptionId parameter separately for ARM clients
423
+ if (param.name.toLowerCase() === "subscriptionid" && dpgContext.arm) {
424
+ isSubscriptionIdAdded = true;
425
+ // For tests, always use env variable
426
+ result.push(
427
+ prepareCommonValue("subscriptionId", subscriptionIdValue, false, true)
428
+ );
429
+ continue;
430
+ }
431
+
432
+ if (!exampleValue || !exampleValue.value) {
433
+ if (!param.optional) {
434
+ // Generate default values for required parameters without examples in tests
435
+ result.push(
436
+ prepareCommonValue(
437
+ param.name,
438
+ `"{Your ${param.name}}"`,
439
+ false,
440
+ param.onClient
441
+ )
442
+ );
443
+ }
444
+ continue;
445
+ }
446
+
447
+ result.push(
448
+ prepareCommonValue(
449
+ exampleValue.parameter.name,
450
+ exampleValue.value,
451
+ param.optional,
452
+ param.onClient
453
+ )
454
+ );
455
+ }
456
+
457
+ // Add subscriptionId for ARM clients if needed
458
+ if (
459
+ dpgContext.arm &&
460
+ getSubscriptionId(dpgContext) &&
461
+ !isSubscriptionIdAdded
462
+ ) {
463
+ result.push(
464
+ prepareCommonValue("subscriptionId", subscriptionIdValue, false, true)
465
+ );
466
+ }
467
+
468
+ // Handle body parameters
469
+ const bodyParam = method.operation.bodyParam;
470
+ const bodySerializeName = bodyParam?.serializedName;
471
+ const bodyExample = parameterMap[bodySerializeName ?? ""];
472
+ if (bodyParam && bodyExample && bodyExample.value) {
473
+ if (
474
+ isSpreadBodyParameter(bodyParam) &&
475
+ bodyParam.type.kind === "model" &&
476
+ bodyExample.value.kind === "model"
477
+ ) {
478
+ for (const prop of bodyParam.type.properties) {
479
+ const propExample = bodyExample.value.value[prop.name];
480
+ if (!propExample) {
481
+ continue;
482
+ }
483
+ // Skip readonly properties as they cannot be set by users
484
+ if (isReadOnly(prop as SdkModelPropertyType)) {
485
+ continue;
486
+ }
487
+ result.push(
488
+ prepareCommonValue(
489
+ prop.name,
490
+ propExample,
491
+ prop.optional,
492
+ prop.onClient
493
+ )
494
+ );
495
+ }
496
+ } else {
497
+ result.push(
498
+ prepareCommonValue(
499
+ bodyParam.name,
500
+ bodyExample.value,
501
+ bodyParam.optional,
502
+ bodyParam.onClient
503
+ )
504
+ );
505
+ }
506
+ }
507
+
508
+ // Handle optional parameters that have examples
509
+ method.operation.parameters
510
+ .filter(
511
+ (param) =>
512
+ param.optional === true &&
513
+ parameterMap[param.serializedName] &&
514
+ !param.clientDefaultValue
515
+ )
516
+ .forEach((param) => {
517
+ const exampleValue = parameterMap[param.serializedName];
518
+ if (exampleValue && exampleValue.value) {
519
+ result.push(
520
+ prepareCommonValue(
521
+ param.name,
522
+ exampleValue.value,
523
+ true,
524
+ param.onClient
525
+ )
526
+ );
527
+ }
528
+ });
529
+
530
+ return result;
531
+ }
532
+
533
+ /**
534
+ * Common client and method iteration logic
535
+ */
536
+ export function iterateClientsAndMethods(
537
+ dpgContext: SdkContext,
538
+ callback: (
539
+ dpgContext: SdkContext,
540
+ method: ServiceOperation,
541
+ options: ClientEmitOptions
542
+ ) => SourceFile | undefined
543
+ ): SourceFile[] {
544
+ const generatedFiles: SourceFile[] = [];
545
+ const clients = dpgContext.sdkPackage.clients;
546
+
547
+ for (const client of clients) {
548
+ const methodMap = getMethodHierarchiesMap(dpgContext, client);
549
+ for (const [prefixKey, methods] of methodMap) {
550
+ const hierarchies = prefixKey ? prefixKey.split("/") : [];
551
+ const prefix = hierarchies
552
+ .map((name) => {
553
+ return normalizeName(name, NameType.Property);
554
+ })
555
+ .join(".");
556
+ for (const method of methods) {
557
+ callback(dpgContext, method, {
558
+ client,
559
+ generatedFiles,
560
+ classicalMethodPrefix: prefix,
561
+ subFolder:
562
+ clients.length > 1
563
+ ? normalizeName(getClassicalClientName(client), NameType.File)
564
+ : undefined,
565
+ hierarchies: hierarchies
566
+ });
567
+ }
568
+ }
569
+ }
570
+ return generatedFiles;
571
+ }
572
+
573
+ /**
574
+ * Generate common method call logic
575
+ */
576
+ export function generateMethodCall(
577
+ method: ServiceOperation,
578
+ parameters: CommonValue[],
579
+ options: ClientEmitOptions,
580
+ dpgContext?: SdkContext
581
+ ): { methodCall: string; clientParams: string[]; clientParamDefs: string[] } {
582
+ // Prepare client-level parameters
583
+ const clientParamValues = parameters.filter((p) => p.onClient);
584
+ const clientParams: string[] = clientParamValues
585
+ .filter((p) => !p.isOptional)
586
+ .map((param) => param.name);
587
+ const clientParamDefs: string[] = clientParamValues
588
+ .filter((p) => !p.isOptional)
589
+ .map((param) => `const ${param.name} = ${param.value};`);
590
+
591
+ // Prepare operation-level parameters
592
+ const methodParamValues = parameters.filter((p) => !p.onClient);
593
+
594
+ let methodParams: string[] = [];
595
+
596
+ // If dpgContext is provided, reorder parameters according to function signature
597
+ if (dpgContext) {
598
+ // Get the actual function signature parameter order
599
+ const operationFunction = getOperationFunction(
600
+ dpgContext,
601
+ [options.hierarchies ?? [], method],
602
+ "Client"
603
+ );
604
+
605
+ // Extract parameter names from the function signature (excluding context and options)
606
+ const signatureParamNames =
607
+ operationFunction.parameters
608
+ ?.filter(
609
+ (p) =>
610
+ p.name !== "context" &&
611
+ !p.type?.toString().includes("OptionalParams")
612
+ )
613
+ .map((p) => p.name) ?? [];
614
+
615
+ // Create a map for quick lookup of parameter values by name
616
+ const paramValueMap = new Map(methodParamValues.map((p) => [p.name, p]));
617
+
618
+ // Reorder methodParamValues according to the signature order
619
+ const orderedRequiredParams = signatureParamNames
620
+ .map((name) => paramValueMap.get(name))
621
+ .filter((p): p is CommonValue => p !== undefined && !p.isOptional);
622
+
623
+ methodParams = orderedRequiredParams.map((p) => `${p.value}`);
624
+ } else {
625
+ // Original logic when dpgContext is not provided
626
+ methodParams = methodParamValues
627
+ .filter((p) => !p.isOptional)
628
+ .map((p) => `${p.value}`);
629
+ }
630
+
631
+ const optionalParams = methodParamValues
632
+ .filter((p) => p.isOptional)
633
+ .map((param) => `${param.name}: ${param.value}`);
634
+ if (optionalParams.length > 0) {
635
+ methodParams.push(`{${optionalParams.join(", ")}}`);
636
+ }
637
+
638
+ const prefix = options.classicalMethodPrefix
639
+ ? `${options.classicalMethodPrefix}.`
640
+ : "";
641
+ const methodCall = `client.${prefix}${normalizeName(method.oriName ?? method.name, NameType.Property)}(${methodParams.join(", ")})`;
642
+
643
+ return { methodCall, clientParams, clientParamDefs };
644
+ }
645
+
646
+ /**
647
+ * Common source file creation logic
648
+ */
649
+ export function createSourceFile(
650
+ dpgContext: SdkContext,
651
+ method: ServiceOperation,
652
+ options: ClientEmitOptions,
653
+ type: "sample" | "test",
654
+ fileName: string
655
+ ): SourceFile {
656
+ const project = useContext("outputProject");
657
+ const operationPrefix = `${options.classicalMethodPrefix ?? ""} ${
658
+ method.oriName ?? method.name
659
+ }`;
660
+ const baseFolder =
661
+ type === "sample" ? "samples-dev" : join("test", "generated");
662
+ const folder = join(
663
+ dpgContext.generationPathDetail?.rootDir ?? "",
664
+ baseFolder,
665
+ options.subFolder ?? ""
666
+ );
667
+ const fileExtension = type === "sample" ? ".ts" : ".spec.ts";
668
+ const normalizedFileName = normalizeName(
669
+ fileName || `${operationPrefix} ${type}`,
670
+ NameType.File
671
+ );
672
+
673
+ return project.createSourceFile(
674
+ join(folder, `${normalizedFileName}${fileExtension}`),
675
+ "",
676
+ { overwrite: true }
677
+ );
678
+ }
679
+
680
+ /**
681
+ * Generate assertions for a specific value (recursive for nested objects)
682
+ */
683
+ export function generateAssertionsForValue(
684
+ value: SdkExampleValue,
685
+ path: string,
686
+ maxDepth: number = 3,
687
+ currentDepth: number = 0
688
+ ): string[] {
689
+ const assertions: string[] = [];
690
+
691
+ // Prevent infinite recursion for deeply nested objects
692
+ if (currentDepth >= maxDepth) {
693
+ return assertions;
694
+ }
695
+
696
+ switch (value.kind) {
697
+ case "string": {
698
+ switch (value.type.kind) {
699
+ case "utcDateTime":
700
+ assertions.push(
701
+ `assert.strictEqual(${path}.getTime(), new Date("${value.value}").getTime());`
702
+ );
703
+ break;
704
+ case "bytes": {
705
+ const encode = value.type.encode ?? "base64";
706
+ assertions.push(
707
+ `assert.deepEqual(${path}, Buffer.from("${value.value}", "${encode}"));`
708
+ );
709
+ break;
710
+ }
711
+ default: {
712
+ const retValue = `"${value.value
713
+ ?.toString()
714
+ .replace(/\\/g, "\\\\")
715
+ .replace(/"/g, '\\"')
716
+ .replace(/\n/g, "\\n")
717
+ .replace(/\r/g, "\\r")
718
+ .replace(/\t/g, "\\t")
719
+ .replace(/\f/g, "\\f")
720
+ .replace(/>/g, ">")
721
+ .replace(/</g, "<")}"`;
722
+ assertions.push(`assert.strictEqual(${path}, ${retValue});`);
723
+ break;
724
+ }
725
+ }
726
+ break;
727
+ }
728
+ case "boolean":
729
+ case "number":
730
+ assertions.push(
731
+ `assert.strictEqual(${path}, ${JSON.stringify(value.value)});`
732
+ );
733
+ break;
734
+ case "unknown":
735
+ // for unknown type we fall back to assert.isDefined to avoid false positives in tests, so we can't assert on the exact value. But we can still check that the payload is defined.
736
+ assertions.push(`assert.isDefined(${path});`);
737
+ break;
738
+ case "array":
739
+ if (value.value && value.value.length > 0) {
740
+ assertions.push(`assert.ok(Array.isArray(${path}));`);
741
+ assertions.push(
742
+ `assert.strictEqual(${path}.length, ${value.value.length});`
743
+ );
744
+
745
+ // Assert on first few items to avoid overly verbose tests
746
+ const itemsToCheck = Math.min(value.value.length, 2);
747
+ for (let i = 0; i < itemsToCheck; i++) {
748
+ const item = value.value[i];
749
+ if (item) {
750
+ const itemAssertions = generateAssertionsForValue(
751
+ item,
752
+ `${path}[${i}]`,
753
+ maxDepth,
754
+ currentDepth + 1
755
+ );
756
+ assertions.push(...itemAssertions);
757
+ }
758
+ }
759
+ }
760
+ break;
761
+
762
+ case "model":
763
+ case "dict":
764
+ if (value.value && typeof value.value === "object") {
765
+ const entries = Object.entries(value.value);
766
+
767
+ for (const [key, val] of entries) {
768
+ if (val && typeof val === "object" && "kind" in val) {
769
+ // Check if this property is flattened in the model type
770
+ let property;
771
+ if (value.kind === "model" && value.type.kind === "model") {
772
+ property = value.type.properties.find(
773
+ (p) => p.kind === "property" && p.name === key
774
+ );
775
+ }
776
+ if (
777
+ property?.flatten &&
778
+ (val as SdkExampleValue).kind === "model"
779
+ ) {
780
+ // For flattened properties, recurse using the parent path so
781
+ // assertions reference result.xxx instead of result.properties.xxx
782
+ const innerAssertions = generateAssertionsForValue(
783
+ val as SdkExampleValue,
784
+ path,
785
+ maxDepth,
786
+ currentDepth + 1
787
+ );
788
+ assertions.push(...innerAssertions);
789
+ } else {
790
+ const propPath = `${path}.${key}`;
791
+ const nestedVal = val as SdkExampleValue;
792
+ // For nested model/dict values, append "?" to the path so child
793
+ // property accesses use optional chaining (e.g. result.systemData?.createdBy)
794
+ const recursePath =
795
+ nestedVal.kind === "model" || nestedVal.kind === "dict"
796
+ ? `${propPath}?`
797
+ : propPath;
798
+ const propAssertions = generateAssertionsForValue(
799
+ nestedVal,
800
+ recursePath,
801
+ maxDepth,
802
+ currentDepth + 1
803
+ );
804
+ assertions.push(...propAssertions);
805
+ }
806
+ }
807
+ }
808
+ }
809
+ break;
810
+
811
+ case "null":
812
+ assertions.push(`assert.strictEqual(${path}, null);`);
813
+ break;
814
+
815
+ case "union":
816
+ // For unions, generate assertions for the actual value
817
+ if (value.value) {
818
+ const unionAssertions = generateAssertionsForValue(
819
+ value.value as SdkExampleValue,
820
+ path,
821
+ maxDepth,
822
+ currentDepth
823
+ );
824
+ assertions.push(...unionAssertions);
825
+ }
826
+ break;
827
+ }
828
+
829
+ return assertions;
830
+ }
831
+
832
+ /**
833
+ * Generate response assertions based on the example responses
834
+ */
835
+ export function generateResponseAssertions(
836
+ example: SdkHttpOperationExample,
837
+ resultVariableName: string,
838
+ isPaging: boolean = false
839
+ ): string[] {
840
+ const assertions: string[] = [];
841
+
842
+ // Get the responses
843
+ const responses = example.responses;
844
+ if (!responses || Object.keys(responses).length === 0) {
845
+ return assertions;
846
+ }
847
+
848
+ // TypeSpec SDK uses numeric indices for responses, get the first response
849
+ const responseKeys = Object.keys(responses);
850
+ if (responseKeys.length === 0) {
851
+ return assertions;
852
+ }
853
+
854
+ const firstResponseKey = responseKeys[0];
855
+ if (!firstResponseKey) {
856
+ return assertions;
857
+ }
858
+
859
+ const firstResponse = (responses as any)[firstResponseKey];
860
+ const responseBody = firstResponse?.bodyValue;
861
+
862
+ if (!responseBody) {
863
+ return assertions;
864
+ }
865
+
866
+ if (isPaging) {
867
+ // For paging operations, the response body should have a 'value' array
868
+ if (responseBody.kind === "model" || responseBody.kind === "dict") {
869
+ const responseValue = responseBody.value as Record<
870
+ string,
871
+ SdkExampleValue
872
+ >;
873
+ const valueArray = responseValue?.["value"];
874
+
875
+ if (valueArray && valueArray.kind === "array" && valueArray.value) {
876
+ // Assert on the length of the collected results
877
+ assertions.push(
878
+ `assert.strictEqual(${resultVariableName}.length, ${valueArray.value.length});`
879
+ );
880
+
881
+ // Assert on the first item if available
882
+ if (valueArray.value.length > 0) {
883
+ const firstItem = valueArray.value[0];
884
+ if (firstItem) {
885
+ const itemAssertions = generateAssertionsForValue(
886
+ firstItem,
887
+ `${resultVariableName}[0]`,
888
+ 2, // Limit depth for paging items
889
+ 0
890
+ );
891
+ assertions.push(...itemAssertions);
892
+ }
893
+ }
894
+ }
895
+ }
896
+ } else {
897
+ // Generate assertions based on response body structure
898
+ const responseAssertions = generateAssertionsForValue(
899
+ responseBody,
900
+ resultVariableName
901
+ );
902
+ assertions.push(...responseAssertions);
903
+ }
904
+
905
+ return assertions;
906
+ }
907
+
908
+ /**
909
+ * Get the environment variable name for a client parameter.
910
+ * Converts camelCase parameter names to UPPER_SNAKE_CASE with optional client name prefix.
911
+ * @param paramName - The parameter name to convert
912
+ * @param clientName - Optional client name to use as prefix
913
+ * @returns The environment variable expression string
914
+ */
915
+ function getEnvironmentVariableName(
916
+ paramName: string,
917
+ clientName?: string
918
+ ): string {
919
+ // Remove "Param" suffix if present
920
+ const cleanName = paramName.replace(/Param$/, "");
921
+
922
+ // Remove "Client" suffix from client name if present and convert to UPPER_SNAKE_CASE
923
+ let prefix = "";
924
+ if (clientName) {
925
+ const cleanClientName = clientName.replace(/Client$/, "");
926
+ prefix =
927
+ cleanClientName
928
+ .replace(/([A-Z])/g, "_$1")
929
+ .toUpperCase()
930
+ .replace(/^_/, "") + "_";
931
+ }
932
+
933
+ // Convert camelCase to UPPER_SNAKE_CASE
934
+ const envVarName = cleanName
935
+ .replace(/([A-Z])/g, "_$1")
936
+ .toUpperCase()
937
+ .replace(/^_/, "");
938
+
939
+ return `process.env.${prefix}${envVarName} || ""`;
940
+ }