@azure-tools/typespec-ts 0.51.1 → 0.52.0

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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@azure-tools/typespec-ts",
3
- "version": "0.51.1",
3
+ "version": "0.52.0",
4
4
  "description": "An experimental TypeSpec emitter for TypeScript RLC",
5
5
  "main": "dist/src/index.js",
6
6
  "type": "module",
@@ -37,7 +37,6 @@
37
37
  "@types/chai": "^4.3.1",
38
38
  "@types/fs-extra": "^9.0.13",
39
39
  "@types/lodash": "^4.17.4",
40
- "@types/mocha": "^10.0.6",
41
40
  "@typescript-eslint/eslint-plugin": "^8.28.0",
42
41
  "@typescript-eslint/parser": "^8.28.0",
43
42
  "@typespec/compiler": "^1.10.0",
@@ -51,13 +50,13 @@
51
50
  "cross-env": "^7.0.3",
52
51
  "eslint-plugin-require-extensions": "0.1.3",
53
52
  "mkdirp": "^3.0.1",
54
- "mocha": "^10.4.0",
55
53
  "npm-run-all": "~4.1.5",
56
54
  "prettier": "^3.3.3",
57
55
  "ts-node": "~10.9.1",
58
- "vitest": "~1.6.0",
59
- "@vitest/coverage-v8": "~1.6.0",
60
- "@vitest/coverage-istanbul": "~1.6.0",
56
+ "vitest": "^4.1.0",
57
+ "vite": "^6.0.0",
58
+ "@vitest/coverage-v8": "^4.1.0",
59
+ "@vitest/coverage-istanbul": "^4.1.0",
61
60
  "@microsoft/api-extractor": "^7.47.5",
62
61
  "tsx": "^4.16.5",
63
62
  "@types/js-yaml": "^4.0.9",
@@ -73,7 +72,7 @@
73
72
  "@typespec/xml": "^0.80.0"
74
73
  },
75
74
  "dependencies": {
76
- "@azure-tools/rlc-common": "^0.51.1",
75
+ "@azure-tools/rlc-common": "^0.52.0",
77
76
  "fast-xml-parser": "^4.5.0",
78
77
  "fs-extra": "^11.1.0",
79
78
  "lodash": "^4.17.21",
@@ -89,19 +88,13 @@
89
88
  "LICENSE",
90
89
  "static/**"
91
90
  ],
92
- "mocha": {
93
- "extension": [
94
- "ts"
95
- ],
96
- "loader": "ts-node/esm"
97
- },
98
91
  "bugs": {
99
92
  "url": "https://github.com/Azure/autorest.typescript/issues"
100
93
  },
101
94
  "homepage": "https://github.com/Azure/autorest.typescript/tree/main/packages/typespec-ts/",
102
95
  "scripts": {
103
- "test-next": "vitest run ./test-next",
104
- "test-next:coverage": "vitest run ./test-next --coverage",
96
+ "test-next": "vitest run --project test-next",
97
+ "test-next:coverage": "vitest run --project test-next --coverage",
105
98
  "clean": "rimraf ./dist ./typespec-output",
106
99
  "build": "tsc -p .",
107
100
  "test": "npm run test-next && npm run unit-test && npm run integration-test-ci",
@@ -136,14 +129,14 @@
136
129
  "generate-tsp-only:azure-modular": "npx tsx ./test/commands/gen-cadl-ranch.js --tag=azure-modular",
137
130
  "regen-test-baselines": "npm run generate-tsp-only",
138
131
  "integration-test:alone": "npm run integration-test:alone:rlc && npm run integration-test:alone:azure-rlc && npm run integration-test:alone:modular && npm run integration-test:alone:azure-modular",
139
- "integration-test:alone:rlc": "cross-env TS_NODE_PROJECT=tsconfig.integration.json mocha -r ts-node/register --experimental-specifier-resolution=node --timeout 36000 ./test/integration/*.spec.ts",
140
- "integration-test:alone:azure-rlc": "cross-env TS_NODE_PROJECT=tsconfig.integration.json mocha -r ts-node/register --experimental-specifier-resolution=node --timeout 36000 ./test/azureIntegration/*.spec.ts",
141
- "integration-test:alone:modular": "cross-env TS_NODE_PROJECT=tsconfig.integration.json mocha -r ts-node/register --experimental-specifier-resolution=node --timeout 36000 ./test/modularIntegration/*.spec.ts",
142
- "integration-test:alone:azure-modular": "cross-env TS_NODE_PROJECT=tsconfig.integration.json mocha -r ts-node/register --experimental-specifier-resolution=node --timeout 36000 ./test/azureModularIntegration/*.spec.ts",
132
+ "integration-test:alone:rlc": "vitest run --project integration-rlc",
133
+ "integration-test:alone:azure-rlc": "vitest run --project integration-azure-rlc",
134
+ "integration-test:alone:modular": "vitest run --project integration-modular",
135
+ "integration-test:alone:azure-modular": "vitest run --project integration-azure-modular",
143
136
  "stop-test-server": "npx tsp-spector server stop",
144
137
  "unit-test": "npm-run-all --parallel unit-test:rlc unit-test:modular",
145
- "unit-test:rlc": "cross-env TS_NODE_PROJECT=tsconfig.json mocha -r ts-node/register --experimental-specifier-resolution=node --experimental-modules=true --timeout 36000 './test/unit/**/*.spec.ts'",
146
- "unit-test:modular": "cross-env TS_NODE_PROJECT=tsconfig.test.json NODE_OPTIONS='--max-old-space-size=8192' mocha -r ts-node/register --experimental-specifier-resolution=node --experimental-modules=true --no-timeout './test/modularUnit/**/*.spec.ts' --reporter-options maxDiffSize=20000",
138
+ "unit-test:rlc": "vitest run --project unit-rlc",
139
+ "unit-test:modular": "cross-env NODE_OPTIONS=--max-old-space-size=8192 vitest run --project unit-modular",
147
140
  "regen-docs": "npm run build && tspd doc . --enable-experimental --output-dir ./website/src/content/docs/docs/emitters/clients/typespec-ts/reference --skip-js"
148
141
  }
149
142
  }
@@ -72,6 +72,7 @@ export function buildClassicalClient(
72
72
  );
73
73
 
74
74
  clientFile.addExportDeclaration({
75
+ isTypeOnly: true,
75
76
  namedExports: [`${classicalClientName}OptionalParams`],
76
77
  moduleSpecifier: `./api/${normalizeName(
77
78
  modularClientName,
@@ -732,13 +732,31 @@ function buildModelInterface(
732
732
  type: SdkModelType
733
733
  ): InterfaceDeclarationStructure {
734
734
  const flattenPropertySet = new Set<SdkModelPropertyType>();
735
+ // For non-input models (output-only, exception, etc.), filter out metadata
736
+ // properties (@header, @query, @path) since they are deserialized separately.
737
+ // For input models, keep metadata properties — users need to pass them.
738
+ const hasInputUsage = (type.usage & UsageFlags.Input) === UsageFlags.Input;
739
+ const isArmResource = isArmResourceModel(type);
735
740
  const interfaceStructure = {
736
741
  kind: StructureKind.Interface,
737
742
  name: normalizeModelName(context, type, NameType.Interface, true),
738
743
  isExported: true,
739
744
  properties: type.properties
740
- .filter((p) => !isMetadata(context.program, p.__raw!))
741
745
  .filter((p) => {
746
+ if (!hasInputUsage && p.__raw && isMetadata(context.program, p.__raw)) {
747
+ return false;
748
+ }
749
+ // Skip the "name" metadata property on ARM resource models.
750
+ // ARM resource "name" is a @path property inherited from the base Resource type
751
+ // and is handled by the ARM infrastructure, not set by the user directly.
752
+ if (
753
+ isArmResource &&
754
+ p.name === "name" &&
755
+ p.__raw &&
756
+ isMetadata(context.program, p.__raw)
757
+ ) {
758
+ return false;
759
+ }
742
760
  // filter out the flatten property to be processed later
743
761
  if (p.flatten && p.type.kind === "model") {
744
762
  flattenPropertySet.add(p);
@@ -757,7 +775,7 @@ function buildModelInterface(
757
775
  context,
758
776
  flatten.type,
759
777
  getAllAncestors(flatten.type)
760
- ).filter((p) => !isMetadata(context.program, p.__raw!));
778
+ );
761
779
  interfaceStructure.properties!.push(
762
780
  ...allProperties.map((p) => {
763
781
  // when the flattened property is optional, all its child properties should be optional too
@@ -932,6 +950,20 @@ export function normalizeModelName(
932
950
  return `${internalModelPrefix}${normalizeName(namespacePrefix + type.name, nameType, true)}${unionSuffix}`;
933
951
  }
934
952
 
953
+ /**
954
+ * Checks if a model descends from the ARM common-types Resource base type
955
+ * (TrackedResource, ProxyResource, etc.) by walking the ancestor chain.
956
+ */
957
+ function isArmResourceModel(type: SdkModelType): boolean {
958
+ const ancestors = getAllAncestors(type);
959
+ return ancestors.some(
960
+ (ancestor) =>
961
+ ancestor.kind === "model" &&
962
+ ancestor.crossLanguageDefinitionId ===
963
+ "Azure.ResourceManager.CommonTypes.Resource"
964
+ );
965
+ }
966
+
935
967
  function buildModelPolymorphicType(context: SdkContext, type: SdkModelType) {
936
968
  // Only include direct subtypes in this union
937
969
  const directSubtypes = getDirectSubtypes(type);
@@ -83,9 +83,9 @@ import {
83
83
  SdkHttpOperation,
84
84
  SdkHttpParameter,
85
85
  SdkLroPagingServiceMethod,
86
+ SdkMethodParameter,
86
87
  SdkLroServiceMethod,
87
88
  SdkMethod,
88
- SdkMethodParameter,
89
89
  SdkModelPropertyType,
90
90
  SdkModelType,
91
91
  SdkPagingServiceMethod,
@@ -98,6 +98,7 @@ import {
98
98
  getHeaderClientOptions,
99
99
  getRestErrorCodeHeader
100
100
  } from "./clientOptionHelpers.js";
101
+ import { getClientParameterName } from "./clientHelpers.js";
101
102
  import { isExtensibleEnum } from "../type-expressions/get-enum-expression.js";
102
103
  import { emitInlineModel } from "../type-expressions/get-model-expression.js";
103
104
 
@@ -205,7 +206,15 @@ export function getDeserializePrivateFunction(
205
206
  let returnType;
206
207
 
207
208
  if (isLroOnly || isLroAndPaging) {
208
- returnType = buildLroReturnType(context, operation);
209
+ if (isLroOnly && shouldWrap) {
210
+ // For LRO-only operations with non-model final result, wrap in a response type alias
211
+ returnType = {
212
+ name: getOperationResponseTypeName(method),
213
+ type: resolveReference(refkey(operation, "response"))
214
+ };
215
+ } else {
216
+ returnType = buildLroReturnType(context, operation);
217
+ }
209
218
  } else if (isPagingOnly && restResponse?.type) {
210
219
  // For paging operations, use the full response model (e.g., _OperationListResult)
211
220
  // instead of just the array element type
@@ -392,7 +401,7 @@ export function getDeserializePrivateFunction(
392
401
  skipDiscriminatedUnionSuffix: false
393
402
  }
394
403
  );
395
- // Handle wrap-non-model-return for non-LRO, non-paging operations
404
+ // Handle wrap-non-model-return for non-LRO, non-paging and LRO-only operations
396
405
  if (shouldWrap) {
397
406
  if (isBinary) {
398
407
  // Binary wrap: getBinaryStream already resolved the stream,
@@ -403,11 +412,12 @@ export function getDeserializePrivateFunction(
403
412
  );
404
413
  } else {
405
414
  // Non-model response: wrap with body property
406
- // Generate the appropriate deserialization for the body value
415
+ // Generate the appropriate deserialization for the body value.
416
+ // For LRO operations, deserializedRoot may include the sub-path (e.g. result.body.someProperty).
407
417
  const bodyValue = deserializeResponseValue(
408
418
  context,
409
419
  deserializedType,
410
- "result.body",
420
+ deserializedRoot,
411
421
  true,
412
422
  getEncodeForType(deserializedType)
413
423
  );
@@ -1217,6 +1227,20 @@ function getLroOnlyOperationFunction(
1217
1227
  const operationStateReference = resolveReference(
1218
1228
  AzurePollingDependencies.OperationState
1219
1229
  );
1230
+
1231
+ // When wrap-non-model-return is enabled and the LRO final result is a non-model type,
1232
+ // use the wrapper response type (e.g. GetIkeSasResponse) instead of the raw type (e.g. string).
1233
+ const { shouldWrap } = checkWrapNonModelReturn(
1234
+ context,
1235
+ operation as ServiceOperation
1236
+ );
1237
+ const effectiveReturnTypeStr = shouldWrap
1238
+ ? (resolveReference(refkey(operation, "response")) as string)
1239
+ : returnType.type;
1240
+ const effectiveReturnTypeName = shouldWrap
1241
+ ? getOperationResponseTypeName(method as [string[], ServiceOperation])
1242
+ : returnType.type;
1243
+
1220
1244
  const functionStatement = {
1221
1245
  kind: StructureKind.Function,
1222
1246
  docs: [
@@ -1228,9 +1252,9 @@ function getLroOnlyOperationFunction(
1228
1252
  name,
1229
1253
  propertyName: normalizeName(operation.name, NameType.Property),
1230
1254
  isLro: true,
1231
- lroFinalReturnType: returnType.type,
1255
+ lroFinalReturnType: effectiveReturnTypeName,
1232
1256
  parameters,
1233
- returnType: `${pollerLikeReference}<${operationStateReference}<${returnType.type}>, ${returnType.type}>`
1257
+ returnType: `${pollerLikeReference}<${operationStateReference}<${effectiveReturnTypeStr}>, ${effectiveReturnTypeStr}>`
1234
1258
  };
1235
1259
 
1236
1260
  const getLongRunningPollerReference = resolveReference(
@@ -1266,9 +1290,7 @@ function getLroOnlyOperationFunction(
1266
1290
  .join(", ")}),
1267
1291
  ${resourceLocationConfig}
1268
1292
  ${apiVersion ? `apiVersion: ${apiVersion}` : ""}
1269
- }) as ${pollerLikeReference}<${operationStateReference}<${
1270
- returnType.type
1271
- }>, ${returnType.type}>;
1293
+ }) as ${pollerLikeReference}<${operationStateReference}<${effectiveReturnTypeStr}>, ${effectiveReturnTypeStr}>;
1272
1294
  `);
1273
1295
 
1274
1296
  return {
@@ -1531,7 +1553,7 @@ function getHeaderAndBodyParameters(
1531
1553
 
1532
1554
  const parametersImplementation: Record<
1533
1555
  "header" | "body",
1534
- { paramMap: string; param: SdkHttpParameter }[]
1556
+ { paramMap: string; param: SdkHttpParameter; paramAccessor: string }[]
1535
1557
  > = {
1536
1558
  header: [],
1537
1559
  body: []
@@ -1556,9 +1578,11 @@ function getHeaderAndBodyParameters(
1556
1578
  param.methodParameterSegments &&
1557
1579
  param.methodParameterSegments.length > 0
1558
1580
  ) {
1581
+ const paramAccessor = getParamAccessor(param, optionalParamName);
1559
1582
  parametersImplementation[param.kind].push({
1560
- paramMap: getParameterMap(dpgContext, param, optionalParamName),
1561
- param
1583
+ paramMap: getParameterMap(dpgContext, param, paramAccessor),
1584
+ param,
1585
+ paramAccessor
1562
1586
  });
1563
1587
  }
1564
1588
  }
@@ -1577,7 +1601,7 @@ function getHeaderAndBodyParameters(
1577
1601
  dpgContext.program,
1578
1602
  i.paramMap,
1579
1603
  i.param,
1580
- optionalParamName
1604
+ i.paramAccessor
1581
1605
  )
1582
1606
  )
1583
1607
  .join(",\n")}, ...${optionalParamName}.requestOptions?.headers },`;
@@ -1600,9 +1624,8 @@ function buildHeaderParameter(
1600
1624
  program: Program,
1601
1625
  paramMap: string,
1602
1626
  param: SdkHttpParameter,
1603
- optionalParamName: string = "options"
1627
+ paramAccessor: string
1604
1628
  ): string {
1605
- const paramName = param.name;
1606
1629
  const effectiveOptional = getEffectiveOptional(param);
1607
1630
  if (!effectiveOptional && isTypeNullable(param.type) === true) {
1608
1631
  reportDiagnostic(program, {
@@ -1611,12 +1634,13 @@ function buildHeaderParameter(
1611
1634
  });
1612
1635
  return paramMap;
1613
1636
  }
1637
+
1614
1638
  const conditions = [];
1615
1639
  if (effectiveOptional) {
1616
- conditions.push(`${optionalParamName}?.${paramName} !== undefined`);
1640
+ conditions.push(`${paramAccessor} !== undefined`);
1617
1641
  }
1618
1642
  if (isTypeNullable(param.type) === true) {
1619
- conditions.push(`${optionalParamName}?.${paramName} !== null`);
1643
+ conditions.push(`${paramAccessor} !== null`);
1620
1644
  }
1621
1645
  return conditions.length > 0
1622
1646
  ? `...(${conditions.join(" && ")} ? {${paramMap}} : {})`
@@ -1747,7 +1771,7 @@ function getEncodingFormat(type: { format?: string }) {
1747
1771
  export function getParameterMap(
1748
1772
  context: SdkContext,
1749
1773
  param: SdkHttpParameter,
1750
- optionalParamName: string = "options"
1774
+ paramAccessor: string
1751
1775
  ): string {
1752
1776
  // Use lowercase for header names since HTTP headers are case-insensitive
1753
1777
  const serializedName =
@@ -1772,18 +1796,18 @@ export function getParameterMap(
1772
1796
  return getCollectionFormatForParam(
1773
1797
  context,
1774
1798
  param,
1775
- optionalParamName,
1799
+ paramAccessor,
1776
1800
  serializedName
1777
1801
  );
1778
1802
  }
1779
1803
 
1780
1804
  // if the parameter or property is optional, we don't need to handle the default value
1781
1805
  if (isOptional(param)) {
1782
- return getOptional(context, param, optionalParamName, serializedName);
1806
+ return getOptional(context, param, serializedName, paramAccessor);
1783
1807
  }
1784
1808
 
1785
1809
  if (isRequired(param)) {
1786
- return getRequired(context, param, serializedName);
1810
+ return getRequired(context, param, serializedName, paramAccessor);
1787
1811
  }
1788
1812
 
1789
1813
  reportDiagnostic(context.program, {
@@ -1802,14 +1826,14 @@ export function getParameterMap(
1802
1826
  function getCollectionFormatForParam(
1803
1827
  context: SdkContext,
1804
1828
  param: SdkHttpParameter,
1805
- optionalParamName: string = "options",
1829
+ paramAccessor: string,
1806
1830
  serializedName: string
1807
1831
  ) {
1808
1832
  const format = (param as any).collectionFormat;
1809
1833
  return `"${serializedName}": ${serializeRequestValue(
1810
1834
  context,
1811
1835
  param.type,
1812
- param.optional ? `${optionalParamName}?.${param.name}` : param.name,
1836
+ paramAccessor,
1813
1837
  !param.optional,
1814
1838
  format,
1815
1839
  serializedName,
@@ -1877,21 +1901,21 @@ function isRequired(param: SdkHttpParameter) {
1877
1901
  function getRequired(
1878
1902
  context: SdkContext,
1879
1903
  param: SdkHttpParameter,
1880
- serializedName: string
1904
+ serializedName: string,
1905
+ paramAccessor: string
1881
1906
  ) {
1882
- const clientValue = `${param.onClient ? "context." : ""}${param.name}`;
1883
1907
  if (param.type.kind === "model") {
1884
1908
  const propertiesStr = getRequestModelMapping(
1885
1909
  context,
1886
1910
  { ...param.type, optional: param.optional },
1887
- clientValue
1911
+ paramAccessor
1888
1912
  );
1889
1913
  return `"${serializedName}": { ${propertiesStr.join(",")} }`;
1890
1914
  }
1891
1915
  return `"${serializedName}": ${serializeRequestValue(
1892
1916
  context,
1893
1917
  param.type,
1894
- clientValue,
1918
+ paramAccessor,
1895
1919
  true,
1896
1920
  getEncodeForType(param.type),
1897
1921
  serializedName,
@@ -1917,11 +1941,9 @@ function isOptional(param: SdkHttpParameter) {
1917
1941
  function getOptional(
1918
1942
  context: SdkContext,
1919
1943
  param: SdkHttpParameter,
1920
- optionalParamName: string,
1921
- serializedName: string
1944
+ serializedName: string,
1945
+ paramAccessor: string
1922
1946
  ) {
1923
- const paramName = `${param.onClient ? "context." : `${optionalParamName}?.`}${param.name}`;
1924
-
1925
1947
  // Apply client default value if present and type matches
1926
1948
  const defaultSuffix =
1927
1949
  param.clientDefaultValue !== undefined &&
@@ -1933,7 +1955,7 @@ function getOptional(
1933
1955
  const propertiesStr = getRequestModelMapping(
1934
1956
  context,
1935
1957
  { ...param.type, optional: param.optional },
1936
- paramName + "?."
1958
+ paramAccessor + "?."
1937
1959
  );
1938
1960
  const serializeContent = `{${propertiesStr.join(",")}}`;
1939
1961
  return `"${serializedName}": ${serializeContent}`;
@@ -1941,7 +1963,7 @@ function getOptional(
1941
1963
  const serializedValue = serializeRequestValue(
1942
1964
  context,
1943
1965
  param.type,
1944
- paramName,
1966
+ paramAccessor,
1945
1967
  false,
1946
1968
  getEncodeForType(param.type),
1947
1969
  serializedName,
@@ -1992,7 +2014,7 @@ function getPathParameters(
1992
2014
  const methodParam = param.methodParameterSegments[0]?.[0];
1993
2015
  if (methodParam) {
1994
2016
  pathParams.push(
1995
- `"${param.serializedName}": ${getPathParamExpr(methodParam, getDefaultValue(param) as string, optionalParamName)}`
2017
+ `"${param.serializedName}": ${getPathParamExpr(param, getDefaultValue(param) as string, optionalParamName)}`
1996
2018
  );
1997
2019
  }
1998
2020
  }
@@ -2028,13 +2050,18 @@ function getQueryParameters(
2028
2050
  param.methodParameterSegments &&
2029
2051
  param.methodParameterSegments.length > 0
2030
2052
  ) {
2053
+ const paramAccessor = getParamAccessor(param);
2031
2054
  parametersImplementation[param.kind].push({
2032
- paramMap: getParameterMap(dpgContext, {
2033
- ...param,
2034
- // TODO: remember to remove this hack once compiler gives us a name
2035
- // https://github.com/microsoft/typespec/issues/6743
2036
- serializedName: getUriTemplateQueryParamName(param.serializedName)
2037
- }),
2055
+ paramMap: getParameterMap(
2056
+ dpgContext,
2057
+ {
2058
+ ...param,
2059
+ // TODO: remember to remove this hack once compiler gives us a name
2060
+ // https://github.com/microsoft/typespec/issues/6743
2061
+ serializedName: getUriTemplateQueryParamName(param.serializedName)
2062
+ },
2063
+ paramAccessor
2064
+ ),
2038
2065
  param
2039
2066
  });
2040
2067
  }
@@ -2057,19 +2084,83 @@ function escapeUriTemplateParamName(name: string) {
2057
2084
  });
2058
2085
  }
2059
2086
 
2087
+ /**
2088
+ * Returns the parameter expression matching the operation signature for an HTTP parameter.
2089
+ * 1. Client-level parameter (`param.onClient`): returns `context.<paramName>`.
2090
+ * 2. Method-level parameter with `methodParameterSegments`: returns the expression
2091
+ * built from those segments (e.g. `options?.ocpDate`, `body.nested`).
2092
+ * 3. Fallback: returns the parameter name directly, with optional chaining if optional.
2093
+ */
2094
+ function getParamAccessor(
2095
+ param: SdkHttpParameter,
2096
+ optionalParamName: string = "options"
2097
+ ): string {
2098
+ const methodParamExpr = getMethodParamExpr(param, optionalParamName);
2099
+ const clientPrefix = "context.";
2100
+ if (methodParamExpr) {
2101
+ return param.onClient
2102
+ ? `${clientPrefix}${methodParamExpr}`
2103
+ : methodParamExpr;
2104
+ }
2105
+ if (param.onClient) {
2106
+ return `${clientPrefix}${getClientParameterName(param)}`;
2107
+ }
2108
+ if (getEffectiveOptional(param)) {
2109
+ return `${optionalParamName}?.${param.name}`;
2110
+ }
2111
+ return param.name;
2112
+ }
2113
+
2114
+ /**
2115
+ * Builds a property accessor expression from the param's `methodParameterSegments`.
2116
+ * Each segment represents a level of property access (e.g. `options?.nested.value`).
2117
+ * Returns `undefined` when no segments are available, so the caller can fall back.
2118
+ */
2119
+ function getMethodParamExpr(
2120
+ param: SdkHttpParameter,
2121
+ optionalParamName: string = "options"
2122
+ ): string | undefined {
2123
+ const segments = param.methodParameterSegments;
2124
+ if (segments.length === 0) {
2125
+ return undefined;
2126
+ }
2127
+ const path = segments[0];
2128
+ if (!path || path.length < 1) {
2129
+ return undefined;
2130
+ }
2131
+
2132
+ const parts: string[] = [];
2133
+ for (let i = 0; i < path.length; i++) {
2134
+ const segment = path[i]!;
2135
+ if (i === 0) {
2136
+ // Normalize names for client-level segments to match the context interface property names
2137
+ const segmentName = segment.onClient
2138
+ ? getClientParameterName(segment as SdkMethodParameter)
2139
+ : segment.name;
2140
+ if (segment.optional && !segment.onClient) {
2141
+ // If the first segment is optional and not on the client, we need to start with the optionalParamName
2142
+ parts.push(`${optionalParamName}?.`);
2143
+ }
2144
+ parts.push(segmentName);
2145
+ } else {
2146
+ const needsOptionalChain = path[i - 1]!.optional;
2147
+ parts.push(`${needsOptionalChain ? "?." : "."}${segment.name}`);
2148
+ }
2149
+ }
2150
+ return parts.join("");
2151
+ }
2152
+
2060
2153
  function getPathParamExpr(
2061
- param: SdkMethodParameter | SdkModelPropertyType,
2154
+ param: SdkHttpParameter,
2062
2155
  defaultValue?: string,
2063
2156
  optionalParamName: string = "options"
2064
2157
  ) {
2065
2158
  if (isConstant(param.type)) {
2066
2159
  return getConstantValue(param.type);
2067
2160
  }
2068
- const paramName = param.onClient
2069
- ? `context.${param.name}`
2070
- : param.optional
2071
- ? `${optionalParamName}["${param.name}"]`
2072
- : param.name;
2161
+
2162
+ const paramName = getParamAccessor(param, optionalParamName);
2163
+
2073
2164
  return defaultValue
2074
2165
  ? typeof defaultValue === "string"
2075
2166
  ? `${paramName} ?? "${defaultValue}"`
@@ -3031,6 +3122,32 @@ export function getOperationResponseTypeName(
3031
3122
  return `${prefix}${normalizeName(operation.name, NameType.Interface)}Response`;
3032
3123
  }
3033
3124
 
3125
+ /**
3126
+ * Returns true when a type should be wrapped with a `body` property.
3127
+ * Wrapping is needed for primitive/enum types; composite (model, dict, model-array)
3128
+ * and unknown-as-record types map to the HLC PropertyKind.Composite / Dictionary
3129
+ * patterns which do NOT get a body wrapper.
3130
+ *
3131
+ * Covered cases (no wrap):
3132
+ * - model array (e.g. Foo[]) → HLC PropertyKind.Composite
3133
+ * - model → HLC PropertyKind.Composite
3134
+ * - dict / Record<string, unknown> → HLC PropertyKind.Dictionary
3135
+ * - unknown with treatUnknownAsRecord → treated as Dict
3136
+ *
3137
+ * Covered cases (wrap):
3138
+ * - string, boolean, number → HLC PropertyKind.Primitive
3139
+ * - string[] → HLC PropertyKind.Primitive (item kind)
3140
+ * - enum / KnownXxx | string → HLC PropertyKind.Enum
3141
+ * - any / unknown (no treatAsRecord) → HLC PropertyKind.Primitive
3142
+ */
3143
+ function isWrappableType(context: SdkContext, type: SdkType): boolean {
3144
+ if (type.kind === "array" && type.valueType.kind === "model") return false;
3145
+ if (type.kind === "dict" || type.kind === "model") return false;
3146
+ if (type.kind === "unknown" && context.rlcOptions?.treatUnknownAsRecord)
3147
+ return false;
3148
+ return true;
3149
+ }
3150
+
3034
3151
  /**
3035
3152
  * Determines whether wrapping the non-model return type is needed for an operation.
3036
3153
  * Returns an object with `shouldWrap` (whether to wrap) and `isBinary` (whether it's a binary response).
@@ -3041,12 +3158,8 @@ export function checkWrapNonModelReturn(
3041
3158
  ): { shouldWrap: boolean; isBinary: boolean } {
3042
3159
  const noWrap = { shouldWrap: false, isBinary: false };
3043
3160
 
3044
- // Only for non-LRO, non-paging normal operations
3045
- if (
3046
- isLroOnlyOperation(operation) ||
3047
- isLroAndPagingOperation(operation) ||
3048
- isPagingOnlyOperation(operation)
3049
- ) {
3161
+ // LRO+paging and paging-only operations are not wrapped
3162
+ if (isLroAndPagingOperation(operation) || isPagingOnlyOperation(operation)) {
3050
3163
  return noWrap;
3051
3164
  }
3052
3165
 
@@ -3055,56 +3168,32 @@ export function checkWrapNonModelReturn(
3055
3168
  return noWrap;
3056
3169
  }
3057
3170
 
3058
- const response = operation.response;
3059
- if (!response.type) {
3171
+ // For LRO-only operations, check the final result type from LRO metadata
3172
+ if (isLroOnlyOperation(operation)) {
3173
+ const lroResultType = operation.lroMetadata?.finalResponse?.result;
3174
+ if (!lroResultType) {
3175
+ return noWrap; // void LRO - no wrap needed
3176
+ }
3177
+ return {
3178
+ shouldWrap: isWrappableType(context, lroResultType),
3179
+ isBinary: false
3180
+ };
3181
+ }
3182
+
3183
+ const { type } = operation.response;
3184
+ if (!type) {
3060
3185
  return noWrap; // void return type - no wrap needed
3061
3186
  }
3062
3187
 
3063
- const type = response.type;
3064
3188
  const contentTypes = operation.operation.responses[0]?.contentTypes ?? [];
3065
3189
 
3066
- // Determine whether to wrap based on the HLC PropertyKind mapping.
3067
- // HLC wraps with `body` only when the type is PropertyKind.Primitive or PropertyKind.Enum
3068
- // (i.e. NOT PropertyKind.Composite or PropertyKind.Dictionary).
3069
- // For array types, HLC uses the item's kind, not the array itself.
3070
- //
3071
- // Case: bytes with binary content type → binary wrap (isBinary=true)
3190
+ // bytes with binary content type binary wrap (isBinary=true)
3072
3191
  // HLC: bytes → binary payload → separate binary handling
3073
3192
  if (type.__raw && isBinaryPayload(context, type.__raw, contentTypes)) {
3074
3193
  return { shouldWrap: true, isBinary: true };
3075
3194
  }
3076
3195
 
3077
- // Case: model array (e.g. Foo[]) no wrap
3078
- // HLC: model array → PropertyKind.Composite (item kind = model) → no body wrapper
3079
- // Modular: array with valueType.kind === "model"
3080
- if (type.kind === "array" && type.valueType.kind === "model") {
3081
- return noWrap;
3082
- }
3083
-
3084
- // Case: any object / Record (e.g. Record<string, unknown>) → no wrap
3085
- // HLC: SchemaType.AnyObject → PropertyKind.Dictionary → no body wrapper
3086
- // Modular: kind === "dict"
3087
- // Case: model → no wrap
3088
- // HLC: model → PropertyKind.Composite → no body wrapper
3089
- // Modular: kind === "model"
3090
- if (type.kind === "dict" || type.kind === "model") {
3091
- return noWrap;
3092
- }
3093
-
3094
- // Case: unknown with treatUnknownAsRecord enabled → no wrap
3095
- // When treatUnknownAsRecord is enabled, `unknown` is treated as Record<string, unknown>
3096
- // which maps to HLC PropertyKind.Dictionary → no body wrapper
3097
- if (type.kind === "unknown" && context.rlcOptions?.treatUnknownAsRecord) {
3098
- return noWrap;
3099
- }
3100
-
3101
- // Remaining cases → wrap with `body`:
3102
- // - string → HLC PropertyKind.Primitive → modular `string`
3103
- // - boolean → HLC PropertyKind.Primitive → modular `boolean`
3104
- // - string[] → HLC PropertyKind.Primitive (array keeps item kind) → modular `string[]`
3105
- // - any → HLC PropertyKind.Primitive → modular `unknown` or `any`
3106
- // - enum → HLC PropertyKind.Enum → modular `KnownXxx | string`
3107
- return { shouldWrap: true, isBinary: false };
3196
+ return { shouldWrap: isWrappableType(context, type), isBinary: false };
3108
3197
  }
3109
3198
 
3110
3199
  /**