Skip to content

Commit 09452f5

Browse files
Implement array encoding for model properties (#3659)
* Implement array encoding * Refine code and update unit tests * Refine code and update tests * Tiny update * Update unit tests for query/header parameters with collection format * Add unit test for report diagnostic case * Refine code * Add a unit test for query parameter * Nit update * Remove throwing exception * Refine code
1 parent 8f58d5a commit 09452f5

19 files changed

Lines changed: 844 additions & 87 deletions

File tree

packages/typespec-ts/src/lib.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -608,6 +608,12 @@ const libDef = {
608608
messages: {
609609
default: paramMessage`Model name conflict detected: "${"modelName"}" exists in multiple namespaces: ${"namespaces"}. Please use @clientName to rename them.`
610610
}
611+
},
612+
"un-supported-array-encoding": {
613+
severity: "warning",
614+
messages: {
615+
default: paramMessage`The array property "${"arrayName"}" of ${"arrayType"} type is not supported for encoding and will be ignored.`
616+
}
611617
}
612618
},
613619
emitter: {

packages/typespec-ts/src/modular/helpers/operationHelpers.ts

Lines changed: 89 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,10 @@ import {
2121
getCollectionFormatHelper,
2222
hasCollectionFormatInfo,
2323
isBinaryPayload,
24-
ServiceOperation
24+
ServiceOperation,
25+
getCollectionFormatParseHelper,
26+
getCollectionFormatFromArrayEncoding,
27+
KnownCollectionFormat
2528
} from "../../utils/operationUtil.js";
2629
import {
2730
getPropertyWithOverrides,
@@ -245,6 +248,7 @@ export function getDeserializePrivateFunction(
245248
context,
246249
deserializedType,
247250
deserializedRoot,
251+
true,
248252
isBinaryPayload(context, response.type!.__raw!, contentTypes!)
249253
? "binary"
250254
: getEncodeForType(deserializedType)
@@ -856,6 +860,7 @@ function buildBodyParameter(
856860
isBinaryPayload(context, bodyParameter.__raw!, bodyParameter.contentTypes)
857861
? "binary"
858862
: getEncodeForType(bodyParameter.type),
863+
undefined,
859864
true
860865
);
861866
return `\nbody: ${serializedBody.startsWith(nullOrUndefinedPrefix) ? "" : nullOrUndefinedPrefix}${serializedBody},`;
@@ -884,7 +889,7 @@ export function getParameterMap(
884889
}
885890

886891
if (hasCollectionFormatInfo(param.kind, (param as any).collectionFormat)) {
887-
return getCollectionFormat(context, param, optionalParamName);
892+
return getCollectionFormatForParam(context, param, optionalParamName);
888893
}
889894

890895
// if the parameter or property is optional, we don't need to handle the default value
@@ -909,39 +914,22 @@ export function getParameterMap(
909914
return `"${param.name}": undefined`;
910915
}
911916

912-
function getCollectionFormat(
917+
function getCollectionFormatForParam(
913918
context: SdkContext,
914919
param: SdkHttpParameter,
915920
optionalParamName: string = "options"
916921
) {
917922
const serializedName = getPropertySerializedName(param);
918923
const format = (param as any).collectionFormat;
919-
const collectionInfo = getCollectionFormatHelper(param.kind, format ?? "");
920-
if (!collectionInfo) {
921-
throw "Has collection format info but without helper function detected";
922-
}
923-
const isMulti = format.toLowerCase() === "multi";
924-
const additionalParam = isMulti ? `, "${serializedName}"` : "";
925-
if (!param.optional) {
926-
return `"${serializedName}": ${collectionInfo}(${serializeRequestValue(
927-
context,
928-
param.type,
929-
param.name,
930-
true,
931-
getEncodeForType(param.type),
932-
true
933-
)}${additionalParam})`;
934-
}
935-
return `"${serializedName}": ${optionalParamName}?.${
936-
param.name
937-
} !== undefined ? ${collectionInfo}(${serializeRequestValue(
924+
return `"${serializedName}": ${serializeRequestValue(
938925
context,
939926
param.type,
940-
`${optionalParamName}?.${param.name}`,
941-
false,
942-
getEncodeForType(param.type),
927+
param.optional ? `${optionalParamName}?.${param.name}` : param.name,
928+
!param.optional,
929+
format,
930+
serializedName,
943931
true
944-
)}${additionalParam}): undefined`;
932+
)}`;
945933
}
946934

947935
function isContentType(param: SdkHttpParameter): boolean {
@@ -992,6 +980,7 @@ function getRequired(context: SdkContext, param: SdkHttpParameter) {
992980
clientValue,
993981
true,
994982
getEncodeForType(param.type),
983+
serializedName,
995984
true
996985
)}`;
997986
}
@@ -1033,6 +1022,7 @@ function getOptional(
10331022
paramName,
10341023
false,
10351024
getEncodeForType(param.type),
1025+
serializedName,
10361026
true
10371027
)}`;
10381028
}
@@ -1172,6 +1162,41 @@ function getNullableCheck(name: string, type: SdkType) {
11721162
return `${name} === null ? null :`;
11731163
}
11741164

1165+
/**
1166+
* Determines the appropriate encoding format for a model property, especially for arrays with collection format encoding.
1167+
* For example, returns "csv" for comma-delimited arrays or the property's type encoding for regular properties.
1168+
*/
1169+
function getEncodeForModelProperty(
1170+
context: SdkContext,
1171+
property: SdkModelPropertyType
1172+
): string | undefined {
1173+
if (property.encode && property.type.kind === "array") {
1174+
// Only arrays of string type can have collectionFormat encoding
1175+
if (property.type.valueType.kind !== "string") {
1176+
reportDiagnostic(context.program, {
1177+
code: "un-supported-array-encoding",
1178+
format: {
1179+
arrayName: property.name,
1180+
arrayType: property.type.valueType.kind
1181+
},
1182+
target: NoTarget
1183+
});
1184+
return getEncodeForType(property.type);
1185+
}
1186+
1187+
const collectionFormat = getCollectionFormatFromArrayEncoding(
1188+
property.encode
1189+
);
1190+
if (
1191+
collectionFormat &&
1192+
hasCollectionFormatInfo(property.kind, collectionFormat)
1193+
) {
1194+
return collectionFormat;
1195+
}
1196+
}
1197+
return getEncodeForType(property.type);
1198+
}
1199+
11751200
function getSerializationExpressionForFlatten(
11761201
context: SdkContext,
11771202
property: SdkModelPropertyType,
@@ -1245,7 +1270,8 @@ export function getSerializationExpression(
12451270
property.type,
12461271
propertyFullName,
12471272
!property.optional,
1248-
getEncodeForType(property.type),
1273+
getEncodeForModelProperty(context, property),
1274+
getPropertySerializedName(property),
12491275
propertyPath === "" ? true : false
12501276
);
12511277
}
@@ -1373,27 +1399,25 @@ export function getResponseMapping(
13731399
context,
13741400
property.type,
13751401
`${propertyPath}${dot}["${serializedName}"]`,
1376-
getEncodeForType(property.type)
1377-
);
1378-
props.push(
1379-
`${propertyName}: ${deserializeValue === `${propertyPath}${dot}["${serializedName}"]` ? "" : nullOrUndefinedPrefix}${deserializeValue}`
1402+
!property.optional,
1403+
getEncodeForModelProperty(context, property)
13801404
);
1405+
props.push(`${propertyName}: ${deserializeValue}`);
13811406
}
13821407
}
13831408
return props;
13841409
}
13851410

13861411
/**
1387-
* This function helps converting strings into JS complex types recursively.
1388-
* We need to drill down into Array elements to make sure that the element type is
1389-
* deserialized correctly
1412+
* Converts JavaScript values to their serialized wire format for HTTP requests.
13901413
*/
13911414
export function serializeRequestValue(
13921415
context: SdkContext,
13931416
type: SdkType,
13941417
clientValue: string,
13951418
required: boolean,
13961419
format?: string,
1420+
serializedName?: string,
13971421
isTopLevel: boolean = false
13981422
): string {
13991423
const getSdkType = useSdkTypes();
@@ -1414,8 +1438,8 @@ export function serializeRequestValue(
14141438
return `${nullOrUndefinedPrefix}${clientValue}.toISOString()`;
14151439
}
14161440
case "array": {
1417-
const prefix = nullOrUndefinedPrefix + clientValue;
14181441
if (type.valueType) {
1442+
const prefix = nullOrUndefinedPrefix + clientValue;
14191443
const elementNullOrUndefinedPrefix =
14201444
isTypeNullable(type.valueType) || getOptionalForType(type.valueType)
14211445
? "!p ? p : "
@@ -1435,7 +1459,17 @@ export function serializeRequestValue(
14351459
) {
14361460
return `${prefix}.map((p: any) => { return ${elementNullOrUndefinedPrefix}p})`;
14371461
} else {
1438-
return `${prefix}.map((p: any) => { return ${elementNullOrUndefinedPrefix}${serializeRequestValue(context, type.valueType, "p", true, getEncodeForType(type.valueType))}})`;
1462+
const serializedValue = `${clientValue}.map((p: any) => { return ${elementNullOrUndefinedPrefix}${serializeRequestValue(context, type.valueType, "p", true, getEncodeForType(type.valueType))}})`;
1463+
if (format) {
1464+
const formatHelper = getCollectionFormatHelper(format);
1465+
if (formatHelper) {
1466+
if (format?.toLowerCase() === KnownCollectionFormat.Multi) {
1467+
return `${nullOrUndefinedPrefix}${formatHelper}(${serializedValue}, "${serializedName}")`;
1468+
}
1469+
return `${nullOrUndefinedPrefix}${formatHelper}(${serializedValue})`;
1470+
}
1471+
}
1472+
return `${nullOrUndefinedPrefix}${serializedValue}`;
14391473
}
14401474
}
14411475
return clientValue;
@@ -1507,14 +1541,15 @@ export function deserializeResponseValue(
15071541
context: SdkContext,
15081542
type: SdkType,
15091543
restValue: string,
1544+
required: boolean,
15101545
format?: string
15111546
): string {
15121547
const dependencies = useDependencies();
15131548
const stringToUint8ArrayReference = resolveReference(
15141549
dependencies.stringToUint8Array
15151550
);
15161551
const nullOrUndefinedPrefix =
1517-
isTypeNullable(type) || getOptionalForType(type)
1552+
isTypeNullable(type) || getOptionalForType(type) || !required
15181553
? `!${restValue}? ${restValue}: `
15191554
: "";
15201555
switch (type.kind) {
@@ -1547,13 +1582,23 @@ export function deserializeResponseValue(
15471582
) {
15481583
return `${prefix}.map((p: any) => { return ${elementNullOrUndefinedPrefix}p})`;
15491584
} else if (type.valueType) {
1550-
return `${prefix}.map((p: any) => { return ${elementNullOrUndefinedPrefix}${deserializeResponseValue(context, type.valueType, "p", getEncodeForType(type.valueType))}})`;
1585+
if (format) {
1586+
const parseHelper = getCollectionFormatParseHelper(format);
1587+
if (parseHelper) {
1588+
// We shouldn't check for an empty string here since an empty string should be parsed as an empty array
1589+
const optionalPrefixForString =
1590+
isTypeNullable(type) || getOptionalForType(type) || !required
1591+
? `${restValue} === null || ${restValue} === undefined ? ${restValue}: `
1592+
: "";
1593+
return `${optionalPrefixForString}${parseHelper}(${restValue})`;
1594+
}
1595+
}
1596+
return `${prefix}.map((p: any) => { return ${elementNullOrUndefinedPrefix}${deserializeResponseValue(context, type.valueType, "p", true, getEncodeForType(type.valueType))}})`;
15511597
} else {
15521598
return restValue;
15531599
}
15541600
}
15551601
case "dict": {
1556-
const prefix = nullOrUndefinedPrefix + restValue;
15571602
let elementNullOrUndefinedPrefix = "";
15581603
if (
15591604
type.valueType &&
@@ -1572,21 +1617,21 @@ export function deserializeResponseValue(
15721617
)
15731618
: undefined;
15741619
if (deserializeFunctionName) {
1575-
return `Object.fromEntries(Object.entries(${prefix}).map(([k, p]: [string, any]) => [k, ${elementNullOrUndefinedPrefix}${deserializeFunctionName}(p)]))`;
1620+
return `${nullOrUndefinedPrefix}Object.fromEntries(Object.entries(${restValue}).map(([k, p]: [string, any]) => [k, ${elementNullOrUndefinedPrefix}${deserializeFunctionName}(p)]))`;
15761621
} else if (
15771622
type.valueType &&
15781623
isAzureCoreErrorType(context.program, type.valueType.__raw)
15791624
) {
1580-
return `Object.fromEntries(Object.entries(${prefix}).map(([k, p]: [string, any]) => [k, ${elementNullOrUndefinedPrefix}p]))`;
1625+
return `${nullOrUndefinedPrefix}Object.fromEntries(Object.entries(${restValue}).map(([k, p]: [string, any]) => [k, ${elementNullOrUndefinedPrefix}p]))`;
15811626
} else if (type.valueType) {
1582-
return `Object.fromEntries(Object.entries(${prefix}).map(([k, p]: [string, any]) => [k, ${elementNullOrUndefinedPrefix}${deserializeResponseValue(context, type.valueType, "p", getEncodeForType(type.valueType))}]))`;
1627+
return `${nullOrUndefinedPrefix}Object.fromEntries(Object.entries(${restValue}).map(([k, p]: [string, any]) => [k, ${elementNullOrUndefinedPrefix}${deserializeResponseValue(context, type.valueType, "p", true, getEncodeForType(type.valueType))}]))`;
15831628
} else {
15841629
return restValue;
15851630
}
15861631
}
15871632
case "bytes":
15881633
if (format !== "binary" && format !== "bytes") {
1589-
return `typeof ${restValue} === 'string'
1634+
return `${nullOrUndefinedPrefix}typeof ${restValue} === 'string'
15901635
? ${stringToUint8ArrayReference}(${restValue}, "${format ?? "base64"}")
15911636
: ${restValue}`;
15921637
}
@@ -1616,6 +1661,7 @@ export function deserializeResponseValue(
16161661
context,
16171662
type.type,
16181663
restValue,
1664+
false,
16191665
getEncodeForType(type.type)
16201666
);
16211667
default:

packages/typespec-ts/src/modular/static-helpers-metadata.ts

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,31 @@ export const SerializationHelpers = {
2424
name: "buildTsvCollection",
2525
location: "serialization/build-tsv-collection.ts"
2626
},
27+
buildNewlineCollection: {
28+
kind: "function",
29+
name: "buildNewlineCollection",
30+
location: "serialization/build-newline-collection.ts"
31+
},
32+
parseCsvCollection: {
33+
kind: "function",
34+
name: "parseCsvCollection",
35+
location: "serialization/parse-csv-collection.ts"
36+
},
37+
parsePipeCollection: {
38+
kind: "function",
39+
name: "parsePipeCollection",
40+
location: "serialization/parse-pipe-collection.ts"
41+
},
42+
parseSsvCollection: {
43+
kind: "function",
44+
name: "parseSsvCollection",
45+
location: "serialization/parse-ssv-collection.ts"
46+
},
47+
parseNewlineCollection: {
48+
kind: "function",
49+
name: "parseNewlineCollection",
50+
location: "serialization/parse-newline-collection.ts"
51+
},
2752
serializeRecord: {
2853
kind: "function",
2954
name: "serializeRecord",

0 commit comments

Comments
 (0)