diff --git a/packages/typespec-test/test/NetworkAnalytics.Management/generated/typespec-ts/sdk/test/arm-test/src/api/dataProducts/index.ts b/packages/typespec-test/test/NetworkAnalytics.Management/generated/typespec-ts/sdk/test/arm-test/src/api/dataProducts/index.ts index 8618b2b551..ebdf8c3865 100644 --- a/packages/typespec-test/test/NetworkAnalytics.Management/generated/typespec-ts/sdk/test/arm-test/src/api/dataProducts/index.ts +++ b/packages/typespec-test/test/NetworkAnalytics.Management/generated/typespec-ts/sdk/test/arm-test/src/api/dataProducts/index.ts @@ -43,6 +43,7 @@ import { buildPagedAsyncIterator, } from "../../static-helpers/pagingHelpers.js"; import { getLongRunningPoller } from "../../static-helpers/pollingHelpers.js"; +import { expandUrlTemplate } from "../../static-helpers/urlTemplate.js"; import { StreamableMethod, PathUncheckedResponse, @@ -59,13 +60,19 @@ export function _createSend( resource: DataProduct, options: DataProductsCreateOptionalParams = { requestOptions: {} }, ): StreamableMethod { + const path = expandUrlTemplate( + "/subscriptions/{subscriptionId}/resourceGroups/{resourceGroupName}/providers/Microsoft.NetworkAnalytics/dataProducts/{dataProductName}{?api-version}", + { + subscriptionId: subscriptionId, + resourceGroupName: resourceGroupName, + dataProductName: dataProductName, + }, + { + allowReserved: options?.requestOptions?.skipUrlEncoding, + }, + ); return context - .path( - "/subscriptions/{subscriptionId}/resourceGroups/{resourceGroupName}/providers/Microsoft.NetworkAnalytics/dataProducts/{dataProductName}", - subscriptionId, - resourceGroupName, - dataProductName, - ) + .path(path) .put({ ...operationOptionsToRequestParameters(options), body: dataProductSerializer(resource), @@ -115,13 +122,19 @@ export function _getSend( dataProductName: string, options: DataProductsGetOptionalParams = { requestOptions: {} }, ): StreamableMethod { + const path = expandUrlTemplate( + "/subscriptions/{subscriptionId}/resourceGroups/{resourceGroupName}/providers/Microsoft.NetworkAnalytics/dataProducts/{dataProductName}{?api-version}", + { + subscriptionId: subscriptionId, + resourceGroupName: resourceGroupName, + dataProductName: dataProductName, + }, + { + allowReserved: options?.requestOptions?.skipUrlEncoding, + }, + ); return context - .path( - "/subscriptions/{subscriptionId}/resourceGroups/{resourceGroupName}/providers/Microsoft.NetworkAnalytics/dataProducts/{dataProductName}", - subscriptionId, - resourceGroupName, - dataProductName, - ) + .path(path) .get({ ...operationOptionsToRequestParameters(options) }); } @@ -162,13 +175,19 @@ export function _updateSend( properties: DataProductUpdate, options: DataProductsUpdateOptionalParams = { requestOptions: {} }, ): StreamableMethod { + const path = expandUrlTemplate( + "/subscriptions/{subscriptionId}/resourceGroups/{resourceGroupName}/providers/Microsoft.NetworkAnalytics/dataProducts/{dataProductName}{?api-version}", + { + subscriptionId: subscriptionId, + resourceGroupName: resourceGroupName, + dataProductName: dataProductName, + }, + { + allowReserved: options?.requestOptions?.skipUrlEncoding, + }, + ); return context - .path( - "/subscriptions/{subscriptionId}/resourceGroups/{resourceGroupName}/providers/Microsoft.NetworkAnalytics/dataProducts/{dataProductName}", - subscriptionId, - resourceGroupName, - dataProductName, - ) + .path(path) .patch({ ...operationOptionsToRequestParameters(options), body: dataProductUpdateSerializer(properties), @@ -218,13 +237,19 @@ export function _$deleteSend( dataProductName: string, options: DataProductsDeleteOptionalParams = { requestOptions: {} }, ): StreamableMethod { + const path = expandUrlTemplate( + "/subscriptions/{subscriptionId}/resourceGroups/{resourceGroupName}/providers/Microsoft.NetworkAnalytics/dataProducts/{dataProductName}{?api-version}", + { + subscriptionId: subscriptionId, + resourceGroupName: resourceGroupName, + dataProductName: dataProductName, + }, + { + allowReserved: options?.requestOptions?.skipUrlEncoding, + }, + ); return context - .path( - "/subscriptions/{subscriptionId}/resourceGroups/{resourceGroupName}/providers/Microsoft.NetworkAnalytics/dataProducts/{dataProductName}", - subscriptionId, - resourceGroupName, - dataProductName, - ) + .path(path) .delete({ ...operationOptionsToRequestParameters(options) }); } @@ -282,13 +307,19 @@ export function _generateStorageAccountSasTokenSend( requestOptions: {}, }, ): StreamableMethod { + const path = expandUrlTemplate( + "/subscriptions/{subscriptionId}/resourceGroups/{resourceGroupName}/providers/Microsoft.NetworkAnalytics/dataProducts/{dataProductName}/generateStorageAccountSasToken{?api-version}", + { + subscriptionId: subscriptionId, + resourceGroupName: resourceGroupName, + dataProductName: dataProductName, + }, + { + allowReserved: options?.requestOptions?.skipUrlEncoding, + }, + ); return context - .path( - "/subscriptions/{subscriptionId}/resourceGroups/{resourceGroupName}/providers/Microsoft.NetworkAnalytics/dataProducts/{dataProductName}/generateStorageAccountSasToken", - subscriptionId, - resourceGroupName, - dataProductName, - ) + .path(path) .post({ ...operationOptionsToRequestParameters(options), body: accountSasSerializer(body), @@ -336,13 +367,19 @@ export function _rotateKeySend( body: KeyVaultInfo, options: DataProductsRotateKeyOptionalParams = { requestOptions: {} }, ): StreamableMethod { + const path = expandUrlTemplate( + "/subscriptions/{subscriptionId}/resourceGroups/{resourceGroupName}/providers/Microsoft.NetworkAnalytics/dataProducts/{dataProductName}/rotateKey{?api-version}", + { + subscriptionId: subscriptionId, + resourceGroupName: resourceGroupName, + dataProductName: dataProductName, + }, + { + allowReserved: options?.requestOptions?.skipUrlEncoding, + }, + ); return context - .path( - "/subscriptions/{subscriptionId}/resourceGroups/{resourceGroupName}/providers/Microsoft.NetworkAnalytics/dataProducts/{dataProductName}/rotateKey", - subscriptionId, - resourceGroupName, - dataProductName, - ) + .path(path) .post({ ...operationOptionsToRequestParameters(options), body: keyVaultInfoSerializer(body), @@ -388,13 +425,19 @@ export function _addUserRoleSend( body: RoleAssignmentCommonProperties, options: DataProductsAddUserRoleOptionalParams = { requestOptions: {} }, ): StreamableMethod { + const path = expandUrlTemplate( + "/subscriptions/{subscriptionId}/resourceGroups/{resourceGroupName}/providers/Microsoft.NetworkAnalytics/dataProducts/{dataProductName}/addUserRole{?api-version}", + { + subscriptionId: subscriptionId, + resourceGroupName: resourceGroupName, + dataProductName: dataProductName, + }, + { + allowReserved: options?.requestOptions?.skipUrlEncoding, + }, + ); return context - .path( - "/subscriptions/{subscriptionId}/resourceGroups/{resourceGroupName}/providers/Microsoft.NetworkAnalytics/dataProducts/{dataProductName}/addUserRole", - subscriptionId, - resourceGroupName, - dataProductName, - ) + .path(path) .post({ ...operationOptionsToRequestParameters(options), body: roleAssignmentCommonPropertiesSerializer(body), @@ -440,13 +483,19 @@ export function _removeUserRoleSend( body: RoleAssignmentDetail, options: DataProductsRemoveUserRoleOptionalParams = { requestOptions: {} }, ): StreamableMethod { + const path = expandUrlTemplate( + "/subscriptions/{subscriptionId}/resourceGroups/{resourceGroupName}/providers/Microsoft.NetworkAnalytics/dataProducts/{dataProductName}/removeUserRole{?api-version}", + { + subscriptionId: subscriptionId, + resourceGroupName: resourceGroupName, + dataProductName: dataProductName, + }, + { + allowReserved: options?.requestOptions?.skipUrlEncoding, + }, + ); return context - .path( - "/subscriptions/{subscriptionId}/resourceGroups/{resourceGroupName}/providers/Microsoft.NetworkAnalytics/dataProducts/{dataProductName}/removeUserRole", - subscriptionId, - resourceGroupName, - dataProductName, - ) + .path(path) .post({ ...operationOptionsToRequestParameters(options), body: roleAssignmentDetailSerializer(body), @@ -494,13 +543,19 @@ export function _listRolesAssignmentsSend( requestOptions: {}, }, ): StreamableMethod { + const path = expandUrlTemplate( + "/subscriptions/{subscriptionId}/resourceGroups/{resourceGroupName}/providers/Microsoft.NetworkAnalytics/dataProducts/{dataProductName}/listRolesAssignments{?api-version}", + { + subscriptionId: subscriptionId, + resourceGroupName: resourceGroupName, + dataProductName: dataProductName, + }, + { + allowReserved: options?.requestOptions?.skipUrlEncoding, + }, + ); return context - .path( - "/subscriptions/{subscriptionId}/resourceGroups/{resourceGroupName}/providers/Microsoft.NetworkAnalytics/dataProducts/{dataProductName}/listRolesAssignments", - subscriptionId, - resourceGroupName, - dataProductName, - ) + .path(path) .post({ ...operationOptionsToRequestParameters(options), body: _listRolesAssignmentsRequestSerializer(body), @@ -548,12 +603,18 @@ export function _listByResourceGroupSend( requestOptions: {}, }, ): StreamableMethod { + const path = expandUrlTemplate( + "/subscriptions/{subscriptionId}/resourceGroups/{resourceGroupName}/providers/Microsoft.NetworkAnalytics/dataProducts{?api-version}", + { + subscriptionId: subscriptionId, + resourceGroupName: resourceGroupName, + }, + { + allowReserved: options?.requestOptions?.skipUrlEncoding, + }, + ); return context - .path( - "/subscriptions/{subscriptionId}/resourceGroups/{resourceGroupName}/providers/Microsoft.NetworkAnalytics/dataProducts", - subscriptionId, - resourceGroupName, - ) + .path(path) .get({ ...operationOptionsToRequestParameters(options) }); } @@ -599,11 +660,17 @@ export function _listBySubscriptionSend( requestOptions: {}, }, ): StreamableMethod { + const path = expandUrlTemplate( + "/subscriptions/{subscriptionId}/providers/Microsoft.NetworkAnalytics/dataProducts{?api-version}", + { + subscriptionId: subscriptionId, + }, + { + allowReserved: options?.requestOptions?.skipUrlEncoding, + }, + ); return context - .path( - "/subscriptions/{subscriptionId}/providers/Microsoft.NetworkAnalytics/dataProducts", - subscriptionId, - ) + .path(path) .get({ ...operationOptionsToRequestParameters(options) }); } diff --git a/packages/typespec-test/test/NetworkAnalytics.Management/generated/typespec-ts/sdk/test/arm-test/src/api/dataProductsCatalogs/index.ts b/packages/typespec-test/test/NetworkAnalytics.Management/generated/typespec-ts/sdk/test/arm-test/src/api/dataProductsCatalogs/index.ts index 832580feac..24588c6573 100644 --- a/packages/typespec-test/test/NetworkAnalytics.Management/generated/typespec-ts/sdk/test/arm-test/src/api/dataProductsCatalogs/index.ts +++ b/packages/typespec-test/test/NetworkAnalytics.Management/generated/typespec-ts/sdk/test/arm-test/src/api/dataProductsCatalogs/index.ts @@ -17,6 +17,7 @@ import { PagedAsyncIterableIterator, buildPagedAsyncIterator, } from "../../static-helpers/pagingHelpers.js"; +import { expandUrlTemplate } from "../../static-helpers/urlTemplate.js"; import { StreamableMethod, PathUncheckedResponse, @@ -30,12 +31,18 @@ export function _getSend( resourceGroupName: string, options: DataProductsCatalogsGetOptionalParams = { requestOptions: {} }, ): StreamableMethod { + const path = expandUrlTemplate( + "/subscriptions/{subscriptionId}/resourceGroups/{resourceGroupName}/providers/Microsoft.NetworkAnalytics/dataProductsCatalogs/default{?api-version}", + { + subscriptionId: subscriptionId, + resourceGroupName: resourceGroupName, + }, + { + allowReserved: options?.requestOptions?.skipUrlEncoding, + }, + ); return context - .path( - "/subscriptions/{subscriptionId}/resourceGroups/{resourceGroupName}/providers/Microsoft.NetworkAnalytics/dataProductsCatalogs/default", - subscriptionId, - resourceGroupName, - ) + .path(path) .get({ ...operationOptionsToRequestParameters(options) }); } @@ -74,12 +81,18 @@ export function _listByResourceGroupSend( requestOptions: {}, }, ): StreamableMethod { + const path = expandUrlTemplate( + "/subscriptions/{subscriptionId}/resourceGroups/{resourceGroupName}/providers/Microsoft.NetworkAnalytics/dataProductsCatalogs{?api-version}", + { + subscriptionId: subscriptionId, + resourceGroupName: resourceGroupName, + }, + { + allowReserved: options?.requestOptions?.skipUrlEncoding, + }, + ); return context - .path( - "/subscriptions/{subscriptionId}/resourceGroups/{resourceGroupName}/providers/Microsoft.NetworkAnalytics/dataProductsCatalogs", - subscriptionId, - resourceGroupName, - ) + .path(path) .get({ ...operationOptionsToRequestParameters(options) }); } @@ -125,11 +138,17 @@ export function _listBySubscriptionSend( requestOptions: {}, }, ): StreamableMethod { + const path = expandUrlTemplate( + "/subscriptions/{subscriptionId}/providers/Microsoft.NetworkAnalytics/dataProductsCatalogs{?api-version}", + { + subscriptionId: subscriptionId, + }, + { + allowReserved: options?.requestOptions?.skipUrlEncoding, + }, + ); return context - .path( - "/subscriptions/{subscriptionId}/providers/Microsoft.NetworkAnalytics/dataProductsCatalogs", - subscriptionId, - ) + .path(path) .get({ ...operationOptionsToRequestParameters(options) }); } diff --git a/packages/typespec-test/test/NetworkAnalytics.Management/generated/typespec-ts/sdk/test/arm-test/src/api/dataTypes/index.ts b/packages/typespec-test/test/NetworkAnalytics.Management/generated/typespec-ts/sdk/test/arm-test/src/api/dataTypes/index.ts index 0e1f70a78f..f5cbe3c165 100644 --- a/packages/typespec-test/test/NetworkAnalytics.Management/generated/typespec-ts/sdk/test/arm-test/src/api/dataTypes/index.ts +++ b/packages/typespec-test/test/NetworkAnalytics.Management/generated/typespec-ts/sdk/test/arm-test/src/api/dataTypes/index.ts @@ -30,6 +30,7 @@ import { buildPagedAsyncIterator, } from "../../static-helpers/pagingHelpers.js"; import { getLongRunningPoller } from "../../static-helpers/pollingHelpers.js"; +import { expandUrlTemplate } from "../../static-helpers/urlTemplate.js"; import { StreamableMethod, PathUncheckedResponse, @@ -47,14 +48,20 @@ export function _createSend( resource: DataType, options: DataTypesCreateOptionalParams = { requestOptions: {} }, ): StreamableMethod { + const path = expandUrlTemplate( + "/subscriptions/{subscriptionId}/resourceGroups/{resourceGroupName}/providers/Microsoft.NetworkAnalytics/dataProducts/{dataProductName}/dataTypes/{dataTypeName}{?api-version}", + { + subscriptionId: subscriptionId, + resourceGroupName: resourceGroupName, + dataProductName: dataProductName, + dataTypeName: dataTypeName, + }, + { + allowReserved: options?.requestOptions?.skipUrlEncoding, + }, + ); return context - .path( - "/subscriptions/{subscriptionId}/resourceGroups/{resourceGroupName}/providers/Microsoft.NetworkAnalytics/dataProducts/{dataProductName}/dataTypes/{dataTypeName}", - subscriptionId, - resourceGroupName, - dataProductName, - dataTypeName, - ) + .path(path) .put({ ...operationOptionsToRequestParameters(options), body: dataTypeSerializer(resource), @@ -107,14 +114,20 @@ export function _getSend( dataTypeName: string, options: DataTypesGetOptionalParams = { requestOptions: {} }, ): StreamableMethod { + const path = expandUrlTemplate( + "/subscriptions/{subscriptionId}/resourceGroups/{resourceGroupName}/providers/Microsoft.NetworkAnalytics/dataProducts/{dataProductName}/dataTypes/{dataTypeName}{?api-version}", + { + subscriptionId: subscriptionId, + resourceGroupName: resourceGroupName, + dataProductName: dataProductName, + dataTypeName: dataTypeName, + }, + { + allowReserved: options?.requestOptions?.skipUrlEncoding, + }, + ); return context - .path( - "/subscriptions/{subscriptionId}/resourceGroups/{resourceGroupName}/providers/Microsoft.NetworkAnalytics/dataProducts/{dataProductName}/dataTypes/{dataTypeName}", - subscriptionId, - resourceGroupName, - dataProductName, - dataTypeName, - ) + .path(path) .get({ ...operationOptionsToRequestParameters(options) }); } @@ -158,14 +171,20 @@ export function _updateSend( properties: DataTypeUpdate, options: DataTypesUpdateOptionalParams = { requestOptions: {} }, ): StreamableMethod { + const path = expandUrlTemplate( + "/subscriptions/{subscriptionId}/resourceGroups/{resourceGroupName}/providers/Microsoft.NetworkAnalytics/dataProducts/{dataProductName}/dataTypes/{dataTypeName}{?api-version}", + { + subscriptionId: subscriptionId, + resourceGroupName: resourceGroupName, + dataProductName: dataProductName, + dataTypeName: dataTypeName, + }, + { + allowReserved: options?.requestOptions?.skipUrlEncoding, + }, + ); return context - .path( - "/subscriptions/{subscriptionId}/resourceGroups/{resourceGroupName}/providers/Microsoft.NetworkAnalytics/dataProducts/{dataProductName}/dataTypes/{dataTypeName}", - subscriptionId, - resourceGroupName, - dataProductName, - dataTypeName, - ) + .path(path) .patch({ ...operationOptionsToRequestParameters(options), body: dataTypeUpdateSerializer(properties), @@ -218,14 +237,20 @@ export function _$deleteSend( dataTypeName: string, options: DataTypesDeleteOptionalParams = { requestOptions: {} }, ): StreamableMethod { + const path = expandUrlTemplate( + "/subscriptions/{subscriptionId}/resourceGroups/{resourceGroupName}/providers/Microsoft.NetworkAnalytics/dataProducts/{dataProductName}/dataTypes/{dataTypeName}{?api-version}", + { + subscriptionId: subscriptionId, + resourceGroupName: resourceGroupName, + dataProductName: dataProductName, + dataTypeName: dataTypeName, + }, + { + allowReserved: options?.requestOptions?.skipUrlEncoding, + }, + ); return context - .path( - "/subscriptions/{subscriptionId}/resourceGroups/{resourceGroupName}/providers/Microsoft.NetworkAnalytics/dataProducts/{dataProductName}/dataTypes/{dataTypeName}", - subscriptionId, - resourceGroupName, - dataProductName, - dataTypeName, - ) + .path(path) .delete({ ...operationOptionsToRequestParameters(options) }); } @@ -284,14 +309,20 @@ export function _deleteDataSend( body: Record, options: DataTypesDeleteDataOptionalParams = { requestOptions: {} }, ): StreamableMethod { + const path = expandUrlTemplate( + "/subscriptions/{subscriptionId}/resourceGroups/{resourceGroupName}/providers/Microsoft.NetworkAnalytics/dataProducts/{dataProductName}/dataTypes/{dataTypeName}/deleteData{?api-version}", + { + subscriptionId: subscriptionId, + resourceGroupName: resourceGroupName, + dataProductName: dataProductName, + dataTypeName: dataTypeName, + }, + { + allowReserved: options?.requestOptions?.skipUrlEncoding, + }, + ); return context - .path( - "/subscriptions/{subscriptionId}/resourceGroups/{resourceGroupName}/providers/Microsoft.NetworkAnalytics/dataProducts/{dataProductName}/dataTypes/{dataTypeName}/deleteData", - subscriptionId, - resourceGroupName, - dataProductName, - dataTypeName, - ) + .path(path) .post({ ...operationOptionsToRequestParameters(options), body: _deleteDataRequestSerializer(body), @@ -352,14 +383,20 @@ export function _generateStorageContainerSasTokenSend( requestOptions: {}, }, ): StreamableMethod { + const path = expandUrlTemplate( + "/subscriptions/{subscriptionId}/resourceGroups/{resourceGroupName}/providers/Microsoft.NetworkAnalytics/dataProducts/{dataProductName}/dataTypes/{dataTypeName}/generateStorageContainerSasToken{?api-version}", + { + subscriptionId: subscriptionId, + resourceGroupName: resourceGroupName, + dataProductName: dataProductName, + dataTypeName: dataTypeName, + }, + { + allowReserved: options?.requestOptions?.skipUrlEncoding, + }, + ); return context - .path( - "/subscriptions/{subscriptionId}/resourceGroups/{resourceGroupName}/providers/Microsoft.NetworkAnalytics/dataProducts/{dataProductName}/dataTypes/{dataTypeName}/generateStorageContainerSasToken", - subscriptionId, - resourceGroupName, - dataProductName, - dataTypeName, - ) + .path(path) .post({ ...operationOptionsToRequestParameters(options), body: containerSaSSerializer(body), @@ -408,13 +445,19 @@ export function _listByDataProductSend( dataProductName: string, options: DataTypesListByDataProductOptionalParams = { requestOptions: {} }, ): StreamableMethod { + const path = expandUrlTemplate( + "/subscriptions/{subscriptionId}/resourceGroups/{resourceGroupName}/providers/Microsoft.NetworkAnalytics/dataProducts/{dataProductName}/dataTypes{?api-version}", + { + subscriptionId: subscriptionId, + resourceGroupName: resourceGroupName, + dataProductName: dataProductName, + }, + { + allowReserved: options?.requestOptions?.skipUrlEncoding, + }, + ); return context - .path( - "/subscriptions/{subscriptionId}/resourceGroups/{resourceGroupName}/providers/Microsoft.NetworkAnalytics/dataProducts/{dataProductName}/dataTypes", - subscriptionId, - resourceGroupName, - dataProductName, - ) + .path(path) .get({ ...operationOptionsToRequestParameters(options) }); } diff --git a/packages/typespec-test/test/NetworkAnalytics.Management/generated/typespec-ts/sdk/test/arm-test/src/static-helpers/urlTemplate.ts b/packages/typespec-test/test/NetworkAnalytics.Management/generated/typespec-ts/sdk/test/arm-test/src/static-helpers/urlTemplate.ts new file mode 100644 index 0000000000..3d56a97a31 --- /dev/null +++ b/packages/typespec-test/test/NetworkAnalytics.Management/generated/typespec-ts/sdk/test/arm-test/src/static-helpers/urlTemplate.ts @@ -0,0 +1,199 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +//--------------------- +// interfaces +//--------------------- +interface ValueOptions { + isFirst: boolean; // is first value in the expression + op?: string; // operator + varValue?: any; // variable value + varName?: string; // variable name + modifier?: string; // modifier e.g * + reserved: boolean; // if true we'll keep reserved words with not encoding +} + +export interface UrlTemplateOptions { + // if set to true, reserved characters will not be encoded + allowReserved?: boolean; +} + +// --------------------- +// helpers +// --------------------- +function encodeValue(val: string, reserved: boolean, op?: string) { + return reserved === true || op === "+" || op === "#" + ? encodeWithReserved(val) + : encodeRFC3986URIComponent(val); +} + +function encodeWithReserved(str: string) { + return str + .split(/(%[0-9A-Fa-f]{2})/g) + .map((part) => (!/%[0-9A-Fa-f]/.test(part) ? encodeURI(part) : part)) + .join(""); +} + +function encodeRFC3986URIComponent(str: string) { + return encodeURIComponent(str).replace( + /[!'()*]/g, + (c) => `%${c.charCodeAt(0).toString(16).toUpperCase()}`, + ); +} + +function isDefined(val: any) { + return val !== undefined && val !== null; +} + +function getNamedAndIfEmpty(op?: string): [boolean, string] { + return [ + !!op && [";", "?", "&"].includes(op), + !!op && ["?", "&"].includes(op) ? "=" : "", + ]; +} + +function getFirstOrSep(op?: string, isFirst = false) { + if (isFirst) { + return !op || op === "+" ? "" : op; + } else if (!op || op === "+" || op === "#") { + return ","; + } else if (op === "?") { + return "&"; + } else { + return op; + } +} + +function getExpandedValue(option: ValueOptions) { + let isFirst = option.isFirst; + const { op, varName, varValue: value, reserved } = option; + const vals: string[] = []; + const [named, ifEmpty] = getNamedAndIfEmpty(op); + + if (Array.isArray(value)) { + for (const val of value.filter(isDefined)) { + // prepare the following parts: separator, varName, value + vals.push(`${getFirstOrSep(op, isFirst)}`); + if (named && varName) { + vals.push(`${encodeURIComponent(varName)}`); + val === "" ? vals.push(ifEmpty) : vals.push("="); + } + vals.push(encodeValue(val, reserved, op)); + isFirst = false; + } + } else if (typeof value === "object") { + for (const key of Object.keys(value)) { + const val = value[key]; + if (!isDefined(val)) { + continue; + } + // prepare the following parts: separator, key, value + vals.push(`${getFirstOrSep(op, isFirst)}`); + if (key) { + vals.push(`${encodeURIComponent(key)}`); + named && val === "" ? vals.push(ifEmpty) : vals.push("="); + } + vals.push(encodeValue(val, reserved, op)); + isFirst = false; + } + } + return vals.join(""); +} + +function getNonExpandedValue(option: ValueOptions) { + const { op, varName, varValue: value, isFirst, reserved } = option; + const vals: string[] = []; + const first = getFirstOrSep(op, isFirst); + const [named, ifEmpty] = getNamedAndIfEmpty(op); + if (named && varName) { + vals.push(encodeValue(varName, reserved, op)); + if (value === "") { + if (!ifEmpty) { + vals.push(ifEmpty); + } + return !vals.join("") ? undefined : `${first}${vals.join("")}`; + } + vals.push("="); + } + + const items = []; + if (Array.isArray(value)) { + for (const val of value.filter(isDefined)) { + items.push(encodeValue(val, reserved, op)); + } + } else if (typeof value === "object") { + for (const key of Object.keys(value)) { + if (!isDefined(value[key])) { + continue; + } + items.push(encodeRFC3986URIComponent(key)); + items.push(encodeValue(value[key], reserved, op)); + } + } + vals.push(items.join(",")); + return !vals.join(",") ? undefined : `${first}${vals.join("")}`; +} + +function getVarValue(option: ValueOptions): string | undefined { + const { op, varName, modifier, isFirst, reserved, varValue: value } = option; + + if (!isDefined(value)) { + return undefined; + } else if (["string", "number", "boolean"].includes(typeof value)) { + let val = value.toString(); + const [named, ifEmpty] = getNamedAndIfEmpty(op); + const vals: string[] = [getFirstOrSep(op, isFirst)]; + if (named && varName) { + vals.push(encodeRFC3986URIComponent(varName)); + val === "" ? vals.push(ifEmpty) : vals.push("="); + } + if (modifier && modifier !== "*") { + val = val.substring(0, parseInt(modifier, 10)); + } + vals.push(encodeValue(val, reserved, op)); + return vals.join(""); + } else if (modifier === "*") { + return getExpandedValue(option); + } else { + return getNonExpandedValue(option); + } +} + +// --------------------------------------------------------------------------------------------------- +// This is an implementation of RFC 6570 URI Template: https://datatracker.ietf.org/doc/html/rfc6570. +// --------------------------------------------------------------------------------------------------- +export function expandUrlTemplate( + template: string, + context: Record, + option?: UrlTemplateOptions, +): string { + return template.replace(/\{([^\{\}]+)\}|([^\{\}]+)/g, (_, expr, text) => { + if (!expr) { + return encodeWithReserved(text); + } + let op; + if (["+", "#", ".", "/", ";", "?", "&"].includes(expr[0])) { + (op = expr[0]), (expr = expr.slice(1)); + } + const varList = expr.split(/,/g); + const result = []; + for (const varSpec of varList) { + const varMatch = /([^:\*]*)(?::(\d+)|(\*))?/.exec(varSpec); + if (!varMatch || !varMatch[1]) { + continue; + } + const varValue = getVarValue({ + isFirst: result.length === 0, + op, + varValue: context[varMatch[1]], + varName: varMatch[1], + modifier: varMatch[2] || varMatch[3], + reserved: option?.allowReserved ?? false, + }); + if (varValue) { + result.push(varValue); + } + } + return result.join(""); + }); +} diff --git a/packages/typespec-test/test/ai/generated/typespec-ts/src/api/agents/index.ts b/packages/typespec-test/test/ai/generated/typespec-ts/src/api/agents/index.ts index aa87b78c7a..fe62c61f46 100644 --- a/packages/typespec-test/test/ai/generated/typespec-ts/src/api/agents/index.ts +++ b/packages/typespec-test/test/ai/generated/typespec-ts/src/api/agents/index.ts @@ -105,6 +105,7 @@ import { VectorStoreFileBatch, vectorStoreFileBatchDeserializer, } from "../../models/models.js"; +import { expandUrlTemplate } from "../../static-helpers/urlTemplate.js"; import { StreamableMethod, PathUncheckedResponse, @@ -195,17 +196,21 @@ export function _listAgentsSend( context: Client, options: AgentsListAgentsOptionalParams = { requestOptions: {} }, ): StreamableMethod { + const path = expandUrlTemplate( + "/assistants{?api-version,limit,order,after,before}", + { + limit: options?.limit, + order: options?.order, + after: options?.after, + before: options?.before, + }, + { + allowReserved: options?.requestOptions?.skipUrlEncoding, + }, + ); return context - .path("/assistants") - .get({ - ...operationOptionsToRequestParameters(options), - queryParameters: { - limit: options?.limit, - order: options?.order, - after: options?.after, - before: options?.before, - }, - }); + .path(path) + .get({ ...operationOptionsToRequestParameters(options) }); } export async function _listAgentsDeserialize( @@ -233,8 +238,17 @@ export function _getAgentSend( assistantId: string, options: AgentsGetAgentOptionalParams = { requestOptions: {} }, ): StreamableMethod { + const path = expandUrlTemplate( + "/assistants/{assistantId}{?api-version}", + { + assistantId: assistantId, + }, + { + allowReserved: options?.requestOptions?.skipUrlEncoding, + }, + ); return context - .path("/assistants/{assistantId}", assistantId) + .path(path) .get({ ...operationOptionsToRequestParameters(options) }); } @@ -264,7 +278,16 @@ export function _updateAgentSend( assistantId: string, options: AgentsUpdateAgentOptionalParams = { requestOptions: {} }, ): StreamableMethod { - return context.path("/assistants/{assistantId}", assistantId).post({ + const path = expandUrlTemplate( + "/assistants/{assistantId}{?api-version}", + { + assistantId: assistantId, + }, + { + allowReserved: options?.requestOptions?.skipUrlEncoding, + }, + ); + return context.path(path).post({ ...operationOptionsToRequestParameters(options), body: { model: options?.model, @@ -342,8 +365,17 @@ export function _deleteAgentSend( assistantId: string, options: AgentsDeleteAgentOptionalParams = { requestOptions: {} }, ): StreamableMethod { + const path = expandUrlTemplate( + "/assistants/{assistantId}{?api-version}", + { + assistantId: assistantId, + }, + { + allowReserved: options?.requestOptions?.skipUrlEncoding, + }, + ); return context - .path("/assistants/{assistantId}", assistantId) + .path(path) .delete({ ...operationOptionsToRequestParameters(options) }); } @@ -442,8 +474,17 @@ export function _getThreadSend( threadId: string, options: AgentsGetThreadOptionalParams = { requestOptions: {} }, ): StreamableMethod { + const path = expandUrlTemplate( + "/threads/{threadId}{?api-version}", + { + threadId: threadId, + }, + { + allowReserved: options?.requestOptions?.skipUrlEncoding, + }, + ); return context - .path("/threads/{threadId}", threadId) + .path(path) .get({ ...operationOptionsToRequestParameters(options) }); } @@ -473,8 +514,17 @@ export function _updateThreadSend( threadId: string, options: AgentsUpdateThreadOptionalParams = { requestOptions: {} }, ): StreamableMethod { + const path = expandUrlTemplate( + "/threads/{threadId}{?api-version}", + { + threadId: threadId, + }, + { + allowReserved: options?.requestOptions?.skipUrlEncoding, + }, + ); return context - .path("/threads/{threadId}", threadId) + .path(path) .post({ ...operationOptionsToRequestParameters(options), body: { @@ -541,8 +591,17 @@ export function _deleteThreadSend( threadId: string, options: AgentsDeleteThreadOptionalParams = { requestOptions: {} }, ): StreamableMethod { + const path = expandUrlTemplate( + "/threads/{threadId}{?api-version}", + { + threadId: threadId, + }, + { + allowReserved: options?.requestOptions?.skipUrlEncoding, + }, + ); return context - .path("/threads/{threadId}", threadId) + .path(path) .delete({ ...operationOptionsToRequestParameters(options) }); } @@ -574,7 +633,16 @@ export function _createMessageSend( content: string, options: AgentsCreateMessageOptionalParams = { requestOptions: {} }, ): StreamableMethod { - return context.path("/threads/{threadId}/messages", threadId).post({ + const path = expandUrlTemplate( + "/threads/{threadId}/messages{?api-version}", + { + threadId: threadId, + }, + { + allowReserved: options?.requestOptions?.skipUrlEncoding, + }, + ); + return context.path(path).post({ ...operationOptionsToRequestParameters(options), body: { role: role, @@ -623,18 +691,23 @@ export function _listMessagesSend( threadId: string, options: AgentsListMessagesOptionalParams = { requestOptions: {} }, ): StreamableMethod { + const path = expandUrlTemplate( + "/threads/{threadId}/messages{?api-version,runId,limit,order,after,before}", + { + threadId: threadId, + runId: options?.runId, + limit: options?.limit, + order: options?.order, + after: options?.after, + before: options?.before, + }, + { + allowReserved: options?.requestOptions?.skipUrlEncoding, + }, + ); return context - .path("/threads/{threadId}/messages", threadId) - .get({ - ...operationOptionsToRequestParameters(options), - queryParameters: { - runId: options?.runId, - limit: options?.limit, - order: options?.order, - after: options?.after, - before: options?.before, - }, - }); + .path(path) + .get({ ...operationOptionsToRequestParameters(options) }); } export async function _listMessagesDeserialize( @@ -664,8 +737,18 @@ export function _getMessageSend( messageId: string, options: AgentsGetMessageOptionalParams = { requestOptions: {} }, ): StreamableMethod { + const path = expandUrlTemplate( + "/threads/{threadId}/messages/{messageId}{?api-version}", + { + threadId: threadId, + messageId: messageId, + }, + { + allowReserved: options?.requestOptions?.skipUrlEncoding, + }, + ); return context - .path("/threads/{threadId}/messages/{messageId}", threadId, messageId) + .path(path) .get({ ...operationOptionsToRequestParameters(options) }); } @@ -697,8 +780,18 @@ export function _updateMessageSend( messageId: string, options: AgentsUpdateMessageOptionalParams = { requestOptions: {} }, ): StreamableMethod { + const path = expandUrlTemplate( + "/threads/{threadId}/messages/{messageId}{?api-version}", + { + threadId: threadId, + messageId: messageId, + }, + { + allowReserved: options?.requestOptions?.skipUrlEncoding, + }, + ); return context - .path("/threads/{threadId}/messages/{messageId}", threadId, messageId) + .path(path) .post({ ...operationOptionsToRequestParameters(options), body: { metadata: options?.metadata }, @@ -738,7 +831,16 @@ export function _createRunSend( assistantId: string, options: AgentsCreateRunOptionalParams = { requestOptions: {} }, ): StreamableMethod { - return context.path("/threads/{threadId}/runs", threadId).post({ + const path = expandUrlTemplate( + "/threads/{threadId}/runs{?api-version}", + { + threadId: threadId, + }, + { + allowReserved: options?.requestOptions?.skipUrlEncoding, + }, + ); + return context.path(path).post({ ...operationOptionsToRequestParameters(options), body: { assistant_id: assistantId, @@ -798,17 +900,22 @@ export function _listRunsSend( threadId: string, options: AgentsListRunsOptionalParams = { requestOptions: {} }, ): StreamableMethod { + const path = expandUrlTemplate( + "/threads/{threadId}/runs{?api-version,limit,order,after,before}", + { + threadId: threadId, + limit: options?.limit, + order: options?.order, + after: options?.after, + before: options?.before, + }, + { + allowReserved: options?.requestOptions?.skipUrlEncoding, + }, + ); return context - .path("/threads/{threadId}/runs", threadId) - .get({ - ...operationOptionsToRequestParameters(options), - queryParameters: { - limit: options?.limit, - order: options?.order, - after: options?.after, - before: options?.before, - }, - }); + .path(path) + .get({ ...operationOptionsToRequestParameters(options) }); } export async function _listRunsDeserialize( @@ -838,8 +945,18 @@ export function _getRunSend( runId: string, options: AgentsGetRunOptionalParams = { requestOptions: {} }, ): StreamableMethod { + const path = expandUrlTemplate( + "/threads/{threadId}/runs/{runId}{?api-version}", + { + threadId: threadId, + runId: runId, + }, + { + allowReserved: options?.requestOptions?.skipUrlEncoding, + }, + ); return context - .path("/threads/{threadId}/runs/{runId}", threadId, runId) + .path(path) .get({ ...operationOptionsToRequestParameters(options) }); } @@ -871,8 +988,18 @@ export function _updateRunSend( runId: string, options: AgentsUpdateRunOptionalParams = { requestOptions: {} }, ): StreamableMethod { + const path = expandUrlTemplate( + "/threads/{threadId}/runs/{runId}{?api-version}", + { + threadId: threadId, + runId: runId, + }, + { + allowReserved: options?.requestOptions?.skipUrlEncoding, + }, + ); return context - .path("/threads/{threadId}/runs/{runId}", threadId, runId) + .path(path) .post({ ...operationOptionsToRequestParameters(options), body: { metadata: options?.metadata }, @@ -908,21 +1035,25 @@ export function _submitToolOutputsToRunSend( toolOutputs: ToolOutput[], options: AgentsSubmitToolOutputsToRunOptionalParams = { requestOptions: {} }, ): StreamableMethod { - return context - .path( - "/threads/{threadId}/runs/{runId}/submit_tool_outputs", - threadId, - runId, - ) - .post({ - ...operationOptionsToRequestParameters(options), - body: { - tool_outputs: toolOutputs.map((p: any) => { - return toolOutputSerializer(p); - }), - stream: options?.stream, - }, - }); + const path = expandUrlTemplate( + "/threads/{threadId}/runs/{runId}/submit_tool_outputs{?api-version}", + { + threadId: threadId, + runId: runId, + }, + { + allowReserved: options?.requestOptions?.skipUrlEncoding, + }, + ); + return context.path(path).post({ + ...operationOptionsToRequestParameters(options), + body: { + tool_outputs: toolOutputs.map((p: any) => { + return toolOutputSerializer(p); + }), + stream: options?.stream, + }, + }); } export async function _submitToolOutputsToRunDeserialize( @@ -960,8 +1091,18 @@ export function _cancelRunSend( runId: string, options: AgentsCancelRunOptionalParams = { requestOptions: {} }, ): StreamableMethod { + const path = expandUrlTemplate( + "/threads/{threadId}/runs/{runId}/cancel{?api-version}", + { + threadId: threadId, + runId: runId, + }, + { + allowReserved: options?.requestOptions?.skipUrlEncoding, + }, + ); return context - .path("/threads/{threadId}/runs/{runId}/cancel", threadId, runId) + .path(path) .post({ ...operationOptionsToRequestParameters(options) }); } @@ -1086,13 +1227,19 @@ export function _getRunStepSend( stepId: string, options: AgentsGetRunStepOptionalParams = { requestOptions: {} }, ): StreamableMethod { + const path = expandUrlTemplate( + "/threads/{threadId}/runs/{runId}/steps/{stepId}{?api-version}", + { + threadId: threadId, + runId: runId, + stepId: stepId, + }, + { + allowReserved: options?.requestOptions?.skipUrlEncoding, + }, + ); return context - .path( - "/threads/{threadId}/runs/{runId}/steps/{stepId}", - threadId, - runId, - stepId, - ) + .path(path) .get({ ...operationOptionsToRequestParameters(options) }); } @@ -1131,17 +1278,23 @@ export function _listRunStepsSend( runId: string, options: AgentsListRunStepsOptionalParams = { requestOptions: {} }, ): StreamableMethod { + const path = expandUrlTemplate( + "/threads/{threadId}/runs/{runId}/steps{?api-version,limit,order,after,before}", + { + threadId: threadId, + runId: runId, + limit: options?.limit, + order: options?.order, + after: options?.after, + before: options?.before, + }, + { + allowReserved: options?.requestOptions?.skipUrlEncoding, + }, + ); return context - .path("/threads/{threadId}/runs/{runId}/steps", threadId, runId) - .get({ - ...operationOptionsToRequestParameters(options), - queryParameters: { - limit: options?.limit, - order: options?.order, - after: options?.after, - before: options?.before, - }, - }); + .path(path) + .get({ ...operationOptionsToRequestParameters(options) }); } export async function _listRunStepsDeserialize( @@ -1170,12 +1323,18 @@ export function _listFilesSend( context: Client, options: AgentsListFilesOptionalParams = { requestOptions: {} }, ): StreamableMethod { + const path = expandUrlTemplate( + "/files{?api-version,purpose}", + { + purpose: options?.purpose, + }, + { + allowReserved: options?.requestOptions?.skipUrlEncoding, + }, + ); return context - .path("/files") - .get({ - ...operationOptionsToRequestParameters(options), - queryParameters: { purpose: options?.purpose }, - }); + .path(path) + .get({ ...operationOptionsToRequestParameters(options) }); } export async function _listFilesDeserialize( @@ -1244,8 +1403,17 @@ export function _deleteFileSend( fileId: string, options: AgentsDeleteFileOptionalParams = { requestOptions: {} }, ): StreamableMethod { + const path = expandUrlTemplate( + "/files/{fileId}{?api-version}", + { + fileId: fileId, + }, + { + allowReserved: options?.requestOptions?.skipUrlEncoding, + }, + ); return context - .path("/files/{fileId}", fileId) + .path(path) .delete({ ...operationOptionsToRequestParameters(options) }); } @@ -1275,8 +1443,17 @@ export function _getFileSend( fileId: string, options: AgentsGetFileOptionalParams = { requestOptions: {} }, ): StreamableMethod { + const path = expandUrlTemplate( + "/files/{fileId}{?api-version}", + { + fileId: fileId, + }, + { + allowReserved: options?.requestOptions?.skipUrlEncoding, + }, + ); return context - .path("/files/{fileId}", fileId) + .path(path) .get({ ...operationOptionsToRequestParameters(options) }); } @@ -1306,8 +1483,17 @@ export function _getFileContentSend( fileId: string, options: AgentsGetFileContentOptionalParams = { requestOptions: {} }, ): StreamableMethod { + const path = expandUrlTemplate( + "/files/{fileId}/content{?api-version}", + { + fileId: fileId, + }, + { + allowReserved: options?.requestOptions?.skipUrlEncoding, + }, + ); return context - .path("/files/{fileId}/content", fileId) + .path(path) .get({ ...operationOptionsToRequestParameters(options) }); } @@ -1336,17 +1522,21 @@ export function _listVectorStoresSend( context: Client, options: AgentsListVectorStoresOptionalParams = { requestOptions: {} }, ): StreamableMethod { + const path = expandUrlTemplate( + "/vector_stores{?api-version,limit,order,after,before}", + { + limit: options?.limit, + order: options?.order, + after: options?.after, + before: options?.before, + }, + { + allowReserved: options?.requestOptions?.skipUrlEncoding, + }, + ); return context - .path("/vector_stores") - .get({ - ...operationOptionsToRequestParameters(options), - queryParameters: { - limit: options?.limit, - order: options?.order, - after: options?.after, - before: options?.before, - }, - }); + .path(path) + .get({ ...operationOptionsToRequestParameters(options) }); } export async function _listVectorStoresDeserialize( @@ -1417,8 +1607,17 @@ export function _getVectorStoreSend( vectorStoreId: string, options: AgentsGetVectorStoreOptionalParams = { requestOptions: {} }, ): StreamableMethod { + const path = expandUrlTemplate( + "/vector_stores/{vectorStoreId}{?api-version}", + { + vectorStoreId: vectorStoreId, + }, + { + allowReserved: options?.requestOptions?.skipUrlEncoding, + }, + ); return context - .path("/vector_stores/{vectorStoreId}", vectorStoreId) + .path(path) .get({ ...operationOptionsToRequestParameters(options) }); } @@ -1448,8 +1647,17 @@ export function _modifyVectorStoreSend( vectorStoreId: string, options: AgentsModifyVectorStoreOptionalParams = { requestOptions: {} }, ): StreamableMethod { + const path = expandUrlTemplate( + "/vector_stores/{vectorStoreId}{?api-version}", + { + vectorStoreId: vectorStoreId, + }, + { + allowReserved: options?.requestOptions?.skipUrlEncoding, + }, + ); return context - .path("/vector_stores/{vectorStoreId}", vectorStoreId) + .path(path) .post({ ...operationOptionsToRequestParameters(options), body: { @@ -1489,8 +1697,17 @@ export function _deleteVectorStoreSend( vectorStoreId: string, options: AgentsDeleteVectorStoreOptionalParams = { requestOptions: {} }, ): StreamableMethod { + const path = expandUrlTemplate( + "/vector_stores/{vectorStoreId}{?api-version}", + { + vectorStoreId: vectorStoreId, + }, + { + allowReserved: options?.requestOptions?.skipUrlEncoding, + }, + ); return context - .path("/vector_stores/{vectorStoreId}", vectorStoreId) + .path(path) .delete({ ...operationOptionsToRequestParameters(options) }); } @@ -1520,18 +1737,23 @@ export function _listVectorStoreFilesSend( vectorStoreId: string, options: AgentsListVectorStoreFilesOptionalParams = { requestOptions: {} }, ): StreamableMethod { + const path = expandUrlTemplate( + "/vector_stores/{vectorStoreId}/files{?api-version,filter,limit,order,after,before}", + { + vectorStoreId: vectorStoreId, + filter: options?.filter, + limit: options?.limit, + order: options?.order, + after: options?.after, + before: options?.before, + }, + { + allowReserved: options?.requestOptions?.skipUrlEncoding, + }, + ); return context - .path("/vector_stores/{vectorStoreId}/files", vectorStoreId) - .get({ - ...operationOptionsToRequestParameters(options), - queryParameters: { - filter: options?.filter, - limit: options?.limit, - order: options?.order, - after: options?.after, - before: options?.before, - }, - }); + .path(path) + .get({ ...operationOptionsToRequestParameters(options) }); } export async function _listVectorStoreFilesDeserialize( @@ -1565,8 +1787,17 @@ export function _createVectorStoreFileSend( fileId: string, options: AgentsCreateVectorStoreFileOptionalParams = { requestOptions: {} }, ): StreamableMethod { + const path = expandUrlTemplate( + "/vector_stores/{vectorStoreId}/files{?api-version}", + { + vectorStoreId: vectorStoreId, + }, + { + allowReserved: options?.requestOptions?.skipUrlEncoding, + }, + ); return context - .path("/vector_stores/{vectorStoreId}/files", vectorStoreId) + .path(path) .post({ ...operationOptionsToRequestParameters(options), body: { @@ -1609,12 +1840,18 @@ export function _getVectorStoreFileSend( fileId: string, options: AgentsGetVectorStoreFileOptionalParams = { requestOptions: {} }, ): StreamableMethod { + const path = expandUrlTemplate( + "/vector_stores/{vectorStoreId}/files/{fileId}{?api-version}", + { + vectorStoreId: vectorStoreId, + fileId: fileId, + }, + { + allowReserved: options?.requestOptions?.skipUrlEncoding, + }, + ); return context - .path( - "/vector_stores/{vectorStoreId}/files/{fileId}", - vectorStoreId, - fileId, - ) + .path(path) .get({ ...operationOptionsToRequestParameters(options) }); } @@ -1651,12 +1888,18 @@ export function _deleteVectorStoreFileSend( fileId: string, options: AgentsDeleteVectorStoreFileOptionalParams = { requestOptions: {} }, ): StreamableMethod { + const path = expandUrlTemplate( + "/vector_stores/{vectorStoreId}/files/{fileId}{?api-version}", + { + vectorStoreId: vectorStoreId, + fileId: fileId, + }, + { + allowReserved: options?.requestOptions?.skipUrlEncoding, + }, + ); return context - .path( - "/vector_stores/{vectorStoreId}/files/{fileId}", - vectorStoreId, - fileId, - ) + .path(path) .delete({ ...operationOptionsToRequestParameters(options) }); } @@ -1698,17 +1941,24 @@ export function _createVectorStoreFileBatchSend( requestOptions: {}, }, ): StreamableMethod { - return context - .path("/vector_stores/{vectorStoreId}/file_batches", vectorStoreId) - .post({ - ...operationOptionsToRequestParameters(options), - body: { - file_ids: fileIds.map((p: any) => { - return p; - }), - chunking_strategy: { type: options?.chunkingStrategy?.["type"] }, - }, - }); + const path = expandUrlTemplate( + "/vector_stores/{vectorStoreId}/file_batches{?api-version}", + { + vectorStoreId: vectorStoreId, + }, + { + allowReserved: options?.requestOptions?.skipUrlEncoding, + }, + ); + return context.path(path).post({ + ...operationOptionsToRequestParameters(options), + body: { + file_ids: fileIds.map((p: any) => { + return p; + }), + chunking_strategy: { type: options?.chunkingStrategy?.["type"] }, + }, + }); } export async function _createVectorStoreFileBatchDeserialize( @@ -1746,12 +1996,18 @@ export function _getVectorStoreFileBatchSend( batchId: string, options: AgentsGetVectorStoreFileBatchOptionalParams = { requestOptions: {} }, ): StreamableMethod { + const path = expandUrlTemplate( + "/vector_stores/{vectorStoreId}/file_batches/{batchId}{?api-version}", + { + vectorStoreId: vectorStoreId, + batchId: batchId, + }, + { + allowReserved: options?.requestOptions?.skipUrlEncoding, + }, + ); return context - .path( - "/vector_stores/{vectorStoreId}/file_batches/{batchId}", - vectorStoreId, - batchId, - ) + .path(path) .get({ ...operationOptionsToRequestParameters(options) }); } @@ -1790,12 +2046,18 @@ export function _cancelVectorStoreFileBatchSend( requestOptions: {}, }, ): StreamableMethod { + const path = expandUrlTemplate( + "/vector_stores/{vectorStoreId}/file_batches/{batchId}/cancel{?api-version}", + { + vectorStoreId: vectorStoreId, + batchId: batchId, + }, + { + allowReserved: options?.requestOptions?.skipUrlEncoding, + }, + ); return context - .path( - "/vector_stores/{vectorStoreId}/file_batches/{batchId}/cancel", - vectorStoreId, - batchId, - ) + .path(path) .post({ ...operationOptionsToRequestParameters(options) }); } @@ -1836,22 +2098,24 @@ export function _listVectorStoreFileBatchFilesSend( requestOptions: {}, }, ): StreamableMethod { + const path = expandUrlTemplate( + "/vector_stores/{vectorStoreId}/file_batches/{batchId}/files{?api-version,filter,limit,order,after,before}", + { + vectorStoreId: vectorStoreId, + batchId: batchId, + filter: options?.filter, + limit: options?.limit, + order: options?.order, + after: options?.after, + before: options?.before, + }, + { + allowReserved: options?.requestOptions?.skipUrlEncoding, + }, + ); return context - .path( - "/vector_stores/{vectorStoreId}/file_batches/{batchId}/files", - vectorStoreId, - batchId, - ) - .get({ - ...operationOptionsToRequestParameters(options), - queryParameters: { - filter: options?.filter, - limit: options?.limit, - order: options?.order, - after: options?.after, - before: options?.before, - }, - }); + .path(path) + .get({ ...operationOptionsToRequestParameters(options) }); } export async function _listVectorStoreFileBatchFilesDeserialize( diff --git a/packages/typespec-test/test/ai/generated/typespec-ts/src/api/connections/index.ts b/packages/typespec-test/test/ai/generated/typespec-ts/src/api/connections/index.ts index 268a08507d..7b3613d65b 100644 --- a/packages/typespec-test/test/ai/generated/typespec-ts/src/api/connections/index.ts +++ b/packages/typespec-test/test/ai/generated/typespec-ts/src/api/connections/index.ts @@ -13,6 +13,7 @@ import { ConnectionsListSecretsResponse, connectionsListSecretsResponseDeserializer, } from "../../models/models.js"; +import { expandUrlTemplate } from "../../static-helpers/urlTemplate.js"; import { StreamableMethod, PathUncheckedResponse, @@ -24,16 +25,20 @@ export function _listSend( context: Client, options: ConnectionsListOptionalParams = { requestOptions: {} }, ): StreamableMethod { + const path = expandUrlTemplate( + "/connections{?api-version,category,includeAll,target}", + { + category: options?.category, + includeAll: options?.includeAll, + target: options?.target, + }, + { + allowReserved: options?.requestOptions?.skipUrlEncoding, + }, + ); return context - .path("/connections") - .get({ - ...operationOptionsToRequestParameters(options), - queryParameters: { - category: options?.category, - includeAll: options?.includeAll, - target: options?.target, - }, - }); + .path(path) + .get({ ...operationOptionsToRequestParameters(options) }); } export async function _listDeserialize( @@ -61,8 +66,17 @@ export function _getSend( connectionName: string, options: ConnectionsGetOptionalParams = { requestOptions: {} }, ): StreamableMethod { + const path = expandUrlTemplate( + "/connections/{connectionName}{?api-version}", + { + connectionName: connectionName, + }, + { + allowReserved: options?.requestOptions?.skipUrlEncoding, + }, + ); return context - .path("/connections/{connectionName}", connectionName) + .path(path) .get({ ...operationOptionsToRequestParameters(options) }); } @@ -93,8 +107,17 @@ export function _listSecretsSend( ignored: string, options: ConnectionsListSecretsOptionalParams = { requestOptions: {} }, ): StreamableMethod { + const path = expandUrlTemplate( + "/connections/{connectionName}/listsecrets{?api-version}", + { + connectionName: connectionName, + }, + { + allowReserved: options?.requestOptions?.skipUrlEncoding, + }, + ); return context - .path("/connections/{connectionName}/listsecrets", connectionName) + .path(path) .post({ ...operationOptionsToRequestParameters(options), body: { ignored: ignored }, diff --git a/packages/typespec-test/test/ai/generated/typespec-ts/src/api/evaluations/index.ts b/packages/typespec-test/test/ai/generated/typespec-ts/src/api/evaluations/index.ts index 810d12b0a4..dac6bd52fd 100644 --- a/packages/typespec-test/test/ai/generated/typespec-ts/src/api/evaluations/index.ts +++ b/packages/typespec-test/test/ai/generated/typespec-ts/src/api/evaluations/index.ts @@ -28,6 +28,7 @@ import { PagedAsyncIterableIterator, buildPagedAsyncIterator, } from "../../static-helpers/pagingHelpers.js"; +import { expandUrlTemplate } from "../../static-helpers/urlTemplate.js"; import { StreamableMethod, PathUncheckedResponse, @@ -40,8 +41,17 @@ export function _getSend( id: string, options: EvaluationsGetOptionalParams = { requestOptions: {} }, ): StreamableMethod { + const path = expandUrlTemplate( + "/evaluations/runs/{id}{?api-version}", + { + id: id, + }, + { + allowReserved: options?.requestOptions?.skipUrlEncoding, + }, + ); return context - .path("/evaluations/runs/{id}", id) + .path(path) .get({ ...operationOptionsToRequestParameters(options), headers: { @@ -111,8 +121,19 @@ export function _listSend( context: Client, options: EvaluationsListOptionalParams = { requestOptions: {} }, ): StreamableMethod { + const path = expandUrlTemplate( + "/evaluations/runs{?api-version,top,skip,maxpagesize}", + { + top: options?.top, + skip: options?.skip, + maxpagesize: options?.maxpagesize, + }, + { + allowReserved: options?.requestOptions?.skipUrlEncoding, + }, + ); return context - .path("/evaluations/runs") + .path(path) .get({ ...operationOptionsToRequestParameters(options), headers: { @@ -120,11 +141,6 @@ export function _listSend( ? { "x-ms-client-request-id": options?.clientRequestId } : {}), }, - queryParameters: { - top: options?.top, - skip: options?.skip, - maxpagesize: options?.maxpagesize, - }, }); } @@ -159,8 +175,17 @@ export function _updateSend( resource: Evaluation, options: EvaluationsUpdateOptionalParams = { requestOptions: {} }, ): StreamableMethod { + const path = expandUrlTemplate( + "/evaluations/runs/{id}{?api-version}", + { + id: id, + }, + { + allowReserved: options?.requestOptions?.skipUrlEncoding, + }, + ); return context - .path("/evaluations/runs/{id}", id) + .path(path) .patch({ ...operationOptionsToRequestParameters(options), contentType: @@ -201,8 +226,17 @@ export function _getScheduleSend( id: string, options: EvaluationsGetScheduleOptionalParams = { requestOptions: {} }, ): StreamableMethod { + const path = expandUrlTemplate( + "/evaluations/schedules/{id}{?api-version}", + { + id: id, + }, + { + allowReserved: options?.requestOptions?.skipUrlEncoding, + }, + ); return context - .path("/evaluations/schedules/{id}", id) + .path(path) .get({ ...operationOptionsToRequestParameters(options), headers: { @@ -242,8 +276,17 @@ export function _createOrReplaceScheduleSend( requestOptions: {}, }, ): StreamableMethod { + const path = expandUrlTemplate( + "/evaluations/schedules/{id}{?api-version}", + { + id: id, + }, + { + allowReserved: options?.requestOptions?.skipUrlEncoding, + }, + ); return context - .path("/evaluations/schedules/{id}", id) + .path(path) .put({ ...operationOptionsToRequestParameters(options), headers: { @@ -288,8 +331,19 @@ export function _listScheduleSend( context: Client, options: EvaluationsListScheduleOptionalParams = { requestOptions: {} }, ): StreamableMethod { + const path = expandUrlTemplate( + "/evaluations/schedules{?api-version,top,skip,maxpagesize}", + { + top: options?.top, + skip: options?.skip, + maxpagesize: options?.maxpagesize, + }, + { + allowReserved: options?.requestOptions?.skipUrlEncoding, + }, + ); return context - .path("/evaluations/schedules") + .path(path) .get({ ...operationOptionsToRequestParameters(options), headers: { @@ -297,11 +351,6 @@ export function _listScheduleSend( ? { "x-ms-client-request-id": options?.clientRequestId } : {}), }, - queryParameters: { - top: options?.top, - skip: options?.skip, - maxpagesize: options?.maxpagesize, - }, }); } @@ -335,8 +384,17 @@ export function _deleteScheduleSend( id: string, options: EvaluationsDeleteScheduleOptionalParams = { requestOptions: {} }, ): StreamableMethod { + const path = expandUrlTemplate( + "/evaluations/schedules/{id}{?api-version}", + { + id: id, + }, + { + allowReserved: options?.requestOptions?.skipUrlEncoding, + }, + ); return context - .path("/evaluations/schedules/{id}", id) + .path(path) .delete({ ...operationOptionsToRequestParameters(options), headers: { diff --git a/packages/typespec-test/test/ai/generated/typespec-ts/src/static-helpers/urlTemplate.ts b/packages/typespec-test/test/ai/generated/typespec-ts/src/static-helpers/urlTemplate.ts new file mode 100644 index 0000000000..3d56a97a31 --- /dev/null +++ b/packages/typespec-test/test/ai/generated/typespec-ts/src/static-helpers/urlTemplate.ts @@ -0,0 +1,199 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +//--------------------- +// interfaces +//--------------------- +interface ValueOptions { + isFirst: boolean; // is first value in the expression + op?: string; // operator + varValue?: any; // variable value + varName?: string; // variable name + modifier?: string; // modifier e.g * + reserved: boolean; // if true we'll keep reserved words with not encoding +} + +export interface UrlTemplateOptions { + // if set to true, reserved characters will not be encoded + allowReserved?: boolean; +} + +// --------------------- +// helpers +// --------------------- +function encodeValue(val: string, reserved: boolean, op?: string) { + return reserved === true || op === "+" || op === "#" + ? encodeWithReserved(val) + : encodeRFC3986URIComponent(val); +} + +function encodeWithReserved(str: string) { + return str + .split(/(%[0-9A-Fa-f]{2})/g) + .map((part) => (!/%[0-9A-Fa-f]/.test(part) ? encodeURI(part) : part)) + .join(""); +} + +function encodeRFC3986URIComponent(str: string) { + return encodeURIComponent(str).replace( + /[!'()*]/g, + (c) => `%${c.charCodeAt(0).toString(16).toUpperCase()}`, + ); +} + +function isDefined(val: any) { + return val !== undefined && val !== null; +} + +function getNamedAndIfEmpty(op?: string): [boolean, string] { + return [ + !!op && [";", "?", "&"].includes(op), + !!op && ["?", "&"].includes(op) ? "=" : "", + ]; +} + +function getFirstOrSep(op?: string, isFirst = false) { + if (isFirst) { + return !op || op === "+" ? "" : op; + } else if (!op || op === "+" || op === "#") { + return ","; + } else if (op === "?") { + return "&"; + } else { + return op; + } +} + +function getExpandedValue(option: ValueOptions) { + let isFirst = option.isFirst; + const { op, varName, varValue: value, reserved } = option; + const vals: string[] = []; + const [named, ifEmpty] = getNamedAndIfEmpty(op); + + if (Array.isArray(value)) { + for (const val of value.filter(isDefined)) { + // prepare the following parts: separator, varName, value + vals.push(`${getFirstOrSep(op, isFirst)}`); + if (named && varName) { + vals.push(`${encodeURIComponent(varName)}`); + val === "" ? vals.push(ifEmpty) : vals.push("="); + } + vals.push(encodeValue(val, reserved, op)); + isFirst = false; + } + } else if (typeof value === "object") { + for (const key of Object.keys(value)) { + const val = value[key]; + if (!isDefined(val)) { + continue; + } + // prepare the following parts: separator, key, value + vals.push(`${getFirstOrSep(op, isFirst)}`); + if (key) { + vals.push(`${encodeURIComponent(key)}`); + named && val === "" ? vals.push(ifEmpty) : vals.push("="); + } + vals.push(encodeValue(val, reserved, op)); + isFirst = false; + } + } + return vals.join(""); +} + +function getNonExpandedValue(option: ValueOptions) { + const { op, varName, varValue: value, isFirst, reserved } = option; + const vals: string[] = []; + const first = getFirstOrSep(op, isFirst); + const [named, ifEmpty] = getNamedAndIfEmpty(op); + if (named && varName) { + vals.push(encodeValue(varName, reserved, op)); + if (value === "") { + if (!ifEmpty) { + vals.push(ifEmpty); + } + return !vals.join("") ? undefined : `${first}${vals.join("")}`; + } + vals.push("="); + } + + const items = []; + if (Array.isArray(value)) { + for (const val of value.filter(isDefined)) { + items.push(encodeValue(val, reserved, op)); + } + } else if (typeof value === "object") { + for (const key of Object.keys(value)) { + if (!isDefined(value[key])) { + continue; + } + items.push(encodeRFC3986URIComponent(key)); + items.push(encodeValue(value[key], reserved, op)); + } + } + vals.push(items.join(",")); + return !vals.join(",") ? undefined : `${first}${vals.join("")}`; +} + +function getVarValue(option: ValueOptions): string | undefined { + const { op, varName, modifier, isFirst, reserved, varValue: value } = option; + + if (!isDefined(value)) { + return undefined; + } else if (["string", "number", "boolean"].includes(typeof value)) { + let val = value.toString(); + const [named, ifEmpty] = getNamedAndIfEmpty(op); + const vals: string[] = [getFirstOrSep(op, isFirst)]; + if (named && varName) { + vals.push(encodeRFC3986URIComponent(varName)); + val === "" ? vals.push(ifEmpty) : vals.push("="); + } + if (modifier && modifier !== "*") { + val = val.substring(0, parseInt(modifier, 10)); + } + vals.push(encodeValue(val, reserved, op)); + return vals.join(""); + } else if (modifier === "*") { + return getExpandedValue(option); + } else { + return getNonExpandedValue(option); + } +} + +// --------------------------------------------------------------------------------------------------- +// This is an implementation of RFC 6570 URI Template: https://datatracker.ietf.org/doc/html/rfc6570. +// --------------------------------------------------------------------------------------------------- +export function expandUrlTemplate( + template: string, + context: Record, + option?: UrlTemplateOptions, +): string { + return template.replace(/\{([^\{\}]+)\}|([^\{\}]+)/g, (_, expr, text) => { + if (!expr) { + return encodeWithReserved(text); + } + let op; + if (["+", "#", ".", "/", ";", "?", "&"].includes(expr[0])) { + (op = expr[0]), (expr = expr.slice(1)); + } + const varList = expr.split(/,/g); + const result = []; + for (const varSpec of varList) { + const varMatch = /([^:\*]*)(?::(\d+)|(\*))?/.exec(varSpec); + if (!varMatch || !varMatch[1]) { + continue; + } + const varValue = getVarValue({ + isFirst: result.length === 0, + op, + varValue: context[varMatch[1]], + varName: varMatch[1], + modifier: varMatch[2] || varMatch[3], + reserved: option?.allowReserved ?? false, + }); + if (varValue) { + result.push(varValue); + } + } + return result.join(""); + }); +} diff --git a/packages/typespec-test/test/anomalyDetector/generated/typespec-ts/src/api/multivariate/index.ts b/packages/typespec-test/test/anomalyDetector/generated/typespec-ts/src/api/multivariate/index.ts index 8ee761c9ca..4f5b2e4caf 100644 --- a/packages/typespec-test/test/anomalyDetector/generated/typespec-ts/src/api/multivariate/index.ts +++ b/packages/typespec-test/test/anomalyDetector/generated/typespec-ts/src/api/multivariate/index.ts @@ -31,6 +31,7 @@ import { PagedAsyncIterableIterator, buildPagedAsyncIterator, } from "../../static-helpers/pagingHelpers.js"; +import { expandUrlTemplate } from "../../static-helpers/urlTemplate.js"; import { StreamableMethod, PathUncheckedResponse, @@ -45,8 +46,17 @@ export function _getMultivariateBatchDetectionResultSend( requestOptions: {}, }, ): StreamableMethod { + const path = expandUrlTemplate( + "/multivariate/detect-batch/{resultId}", + { + resultId: resultId, + }, + { + allowReserved: options?.requestOptions?.skipUrlEncoding, + }, + ); return context - .path("/multivariate/detect-batch/{resultId}", resultId) + .path(path) .get({ ...operationOptionsToRequestParameters(options) }); } @@ -132,12 +142,19 @@ export function _listMultivariateModelsSend( requestOptions: {}, }, ): StreamableMethod { + const path = expandUrlTemplate( + "/multivariate/models{?skip,top}", + { + skip: options?.skip, + top: options?.top, + }, + { + allowReserved: options?.requestOptions?.skipUrlEncoding, + }, + ); return context - .path("/multivariate/models") - .get({ - ...operationOptionsToRequestParameters(options), - queryParameters: { skip: options?.skip, top: options?.top }, - }); + .path(path) + .get({ ...operationOptionsToRequestParameters(options) }); } export async function _listMultivariateModelsDeserialize( @@ -174,8 +191,17 @@ export function _deleteMultivariateModelSend( requestOptions: {}, }, ): StreamableMethod { + const path = expandUrlTemplate( + "/multivariate/models/{modelId}", + { + modelId: modelId, + }, + { + allowReserved: options?.requestOptions?.skipUrlEncoding, + }, + ); return context - .path("/multivariate/models/{modelId}", modelId) + .path(path) .delete({ ...operationOptionsToRequestParameters(options) }); } @@ -209,8 +235,17 @@ export function _getMultivariateModelSend( requestOptions: {}, }, ): StreamableMethod { + const path = expandUrlTemplate( + "/multivariate/models/{modelId}", + { + modelId: modelId, + }, + { + allowReserved: options?.requestOptions?.skipUrlEncoding, + }, + ); return context - .path("/multivariate/models/{modelId}", modelId) + .path(path) .get({ ...operationOptionsToRequestParameters(options) }); } @@ -248,8 +283,17 @@ export function _detectMultivariateBatchAnomalySend( requestOptions: {}, }, ): StreamableMethod { + const path = expandUrlTemplate( + "/multivariate/models/{modelId}:detect-batch", + { + modelId: modelId, + }, + { + allowReserved: optionalParams?.requestOptions?.skipUrlEncoding, + }, + ); return context - .path("/multivariate/models/{modelId}:detect-batch", modelId) + .path(path) .post({ ...operationOptionsToRequestParameters(optionalParams), body: multivariateMultivariateBatchDetectionOptionsSerializer(options), @@ -300,8 +344,17 @@ export function _detectMultivariateLastAnomalySend( requestOptions: {}, }, ): StreamableMethod { + const path = expandUrlTemplate( + "/multivariate/models/{modelId}:detect-last", + { + modelId: modelId, + }, + { + allowReserved: optionalParams?.requestOptions?.skipUrlEncoding, + }, + ); return context - .path("/multivariate/models/{modelId}:detect-last", modelId) + .path(path) .post({ ...operationOptionsToRequestParameters(optionalParams), body: multivariateMultivariateLastDetectionOptionsSerializer(options), diff --git a/packages/typespec-test/test/anomalyDetector/generated/typespec-ts/src/static-helpers/urlTemplate.ts b/packages/typespec-test/test/anomalyDetector/generated/typespec-ts/src/static-helpers/urlTemplate.ts new file mode 100644 index 0000000000..3d56a97a31 --- /dev/null +++ b/packages/typespec-test/test/anomalyDetector/generated/typespec-ts/src/static-helpers/urlTemplate.ts @@ -0,0 +1,199 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +//--------------------- +// interfaces +//--------------------- +interface ValueOptions { + isFirst: boolean; // is first value in the expression + op?: string; // operator + varValue?: any; // variable value + varName?: string; // variable name + modifier?: string; // modifier e.g * + reserved: boolean; // if true we'll keep reserved words with not encoding +} + +export interface UrlTemplateOptions { + // if set to true, reserved characters will not be encoded + allowReserved?: boolean; +} + +// --------------------- +// helpers +// --------------------- +function encodeValue(val: string, reserved: boolean, op?: string) { + return reserved === true || op === "+" || op === "#" + ? encodeWithReserved(val) + : encodeRFC3986URIComponent(val); +} + +function encodeWithReserved(str: string) { + return str + .split(/(%[0-9A-Fa-f]{2})/g) + .map((part) => (!/%[0-9A-Fa-f]/.test(part) ? encodeURI(part) : part)) + .join(""); +} + +function encodeRFC3986URIComponent(str: string) { + return encodeURIComponent(str).replace( + /[!'()*]/g, + (c) => `%${c.charCodeAt(0).toString(16).toUpperCase()}`, + ); +} + +function isDefined(val: any) { + return val !== undefined && val !== null; +} + +function getNamedAndIfEmpty(op?: string): [boolean, string] { + return [ + !!op && [";", "?", "&"].includes(op), + !!op && ["?", "&"].includes(op) ? "=" : "", + ]; +} + +function getFirstOrSep(op?: string, isFirst = false) { + if (isFirst) { + return !op || op === "+" ? "" : op; + } else if (!op || op === "+" || op === "#") { + return ","; + } else if (op === "?") { + return "&"; + } else { + return op; + } +} + +function getExpandedValue(option: ValueOptions) { + let isFirst = option.isFirst; + const { op, varName, varValue: value, reserved } = option; + const vals: string[] = []; + const [named, ifEmpty] = getNamedAndIfEmpty(op); + + if (Array.isArray(value)) { + for (const val of value.filter(isDefined)) { + // prepare the following parts: separator, varName, value + vals.push(`${getFirstOrSep(op, isFirst)}`); + if (named && varName) { + vals.push(`${encodeURIComponent(varName)}`); + val === "" ? vals.push(ifEmpty) : vals.push("="); + } + vals.push(encodeValue(val, reserved, op)); + isFirst = false; + } + } else if (typeof value === "object") { + for (const key of Object.keys(value)) { + const val = value[key]; + if (!isDefined(val)) { + continue; + } + // prepare the following parts: separator, key, value + vals.push(`${getFirstOrSep(op, isFirst)}`); + if (key) { + vals.push(`${encodeURIComponent(key)}`); + named && val === "" ? vals.push(ifEmpty) : vals.push("="); + } + vals.push(encodeValue(val, reserved, op)); + isFirst = false; + } + } + return vals.join(""); +} + +function getNonExpandedValue(option: ValueOptions) { + const { op, varName, varValue: value, isFirst, reserved } = option; + const vals: string[] = []; + const first = getFirstOrSep(op, isFirst); + const [named, ifEmpty] = getNamedAndIfEmpty(op); + if (named && varName) { + vals.push(encodeValue(varName, reserved, op)); + if (value === "") { + if (!ifEmpty) { + vals.push(ifEmpty); + } + return !vals.join("") ? undefined : `${first}${vals.join("")}`; + } + vals.push("="); + } + + const items = []; + if (Array.isArray(value)) { + for (const val of value.filter(isDefined)) { + items.push(encodeValue(val, reserved, op)); + } + } else if (typeof value === "object") { + for (const key of Object.keys(value)) { + if (!isDefined(value[key])) { + continue; + } + items.push(encodeRFC3986URIComponent(key)); + items.push(encodeValue(value[key], reserved, op)); + } + } + vals.push(items.join(",")); + return !vals.join(",") ? undefined : `${first}${vals.join("")}`; +} + +function getVarValue(option: ValueOptions): string | undefined { + const { op, varName, modifier, isFirst, reserved, varValue: value } = option; + + if (!isDefined(value)) { + return undefined; + } else if (["string", "number", "boolean"].includes(typeof value)) { + let val = value.toString(); + const [named, ifEmpty] = getNamedAndIfEmpty(op); + const vals: string[] = [getFirstOrSep(op, isFirst)]; + if (named && varName) { + vals.push(encodeRFC3986URIComponent(varName)); + val === "" ? vals.push(ifEmpty) : vals.push("="); + } + if (modifier && modifier !== "*") { + val = val.substring(0, parseInt(modifier, 10)); + } + vals.push(encodeValue(val, reserved, op)); + return vals.join(""); + } else if (modifier === "*") { + return getExpandedValue(option); + } else { + return getNonExpandedValue(option); + } +} + +// --------------------------------------------------------------------------------------------------- +// This is an implementation of RFC 6570 URI Template: https://datatracker.ietf.org/doc/html/rfc6570. +// --------------------------------------------------------------------------------------------------- +export function expandUrlTemplate( + template: string, + context: Record, + option?: UrlTemplateOptions, +): string { + return template.replace(/\{([^\{\}]+)\}|([^\{\}]+)/g, (_, expr, text) => { + if (!expr) { + return encodeWithReserved(text); + } + let op; + if (["+", "#", ".", "/", ";", "?", "&"].includes(expr[0])) { + (op = expr[0]), (expr = expr.slice(1)); + } + const varList = expr.split(/,/g); + const result = []; + for (const varSpec of varList) { + const varMatch = /([^:\*]*)(?::(\d+)|(\*))?/.exec(varSpec); + if (!varMatch || !varMatch[1]) { + continue; + } + const varValue = getVarValue({ + isFirst: result.length === 0, + op, + varValue: context[varMatch[1]], + varName: varMatch[1], + modifier: varMatch[2] || varMatch[3], + reserved: option?.allowReserved ?? false, + }); + if (varValue) { + result.push(varValue); + } + } + return result.join(""); + }); +} diff --git a/packages/typespec-test/test/batch_modular/generated/typespec-ts/src/api/operations.ts b/packages/typespec-test/test/batch_modular/generated/typespec-ts/src/api/operations.ts index 30f6aae1d0..95b8dca757 100644 --- a/packages/typespec-test/test/batch_modular/generated/typespec-ts/src/api/operations.ts +++ b/packages/typespec-test/test/batch_modular/generated/typespec-ts/src/api/operations.ts @@ -187,6 +187,7 @@ import { PagedAsyncIterableIterator, buildPagedAsyncIterator, } from "../static-helpers/pagingHelpers.js"; +import { expandUrlTemplate } from "../static-helpers/urlTemplate.js"; import { StreamableMethod, PathUncheckedResponse, @@ -199,16 +200,20 @@ export function _listApplicationsSend( context: Client, options: ListApplicationsOptionalParams = { requestOptions: {} }, ): StreamableMethod { + const path = expandUrlTemplate( + "/applications{?api-version,maxresults,timeOut}", + { + "api-version": options?.apiVersion ?? "2023-05-01.17.0", + maxresults: options?.maxresults, + timeOut: options?.timeOutInSeconds, + }, + { + allowReserved: options?.requestOptions?.skipUrlEncoding, + }, + ); return context - .path("/applications") - .get({ - ...operationOptionsToRequestParameters(options), - queryParameters: { - "api-version": options?.apiVersion ?? "2023-05-01.17.0", - maxresults: options?.maxresults, - timeOut: options?.timeOutInSeconds, - }, - }); + .path(path) + .get({ ...operationOptionsToRequestParameters(options) }); } export async function _listApplicationsDeserialize( @@ -247,15 +252,20 @@ export function _getApplicationSend( applicationId: string, options: GetApplicationOptionalParams = { requestOptions: {} }, ): StreamableMethod { + const path = expandUrlTemplate( + "/applications/{applicationId}{?api-version,timeOut}", + { + applicationId: applicationId, + "api-version": options?.apiVersion ?? "2023-05-01.17.0", + timeOut: options?.timeOutInSeconds, + }, + { + allowReserved: options?.requestOptions?.skipUrlEncoding, + }, + ); return context - .path("/applications/{applicationId}", applicationId) - .get({ - ...operationOptionsToRequestParameters(options), - queryParameters: { - "api-version": options?.apiVersion ?? "2023-05-01.17.0", - timeOut: options?.timeOutInSeconds, - }, - }); + .path(path) + .get({ ...operationOptionsToRequestParameters(options) }); } export async function _getApplicationDeserialize( @@ -289,19 +299,23 @@ export function _listPoolUsageMetricsSend( context: Client, options: ListPoolUsageMetricsOptionalParams = { requestOptions: {} }, ): StreamableMethod { + const path = expandUrlTemplate( + "/poolusagemetrics{?api-version,maxresults,timeOut,starttime,endtime,$filter}", + { + "api-version": options?.apiVersion ?? "2023-05-01.17.0", + maxresults: options?.maxresults, + timeOut: options?.timeOutInSeconds, + starttime: options?.starttime?.toISOString(), + endtime: options?.endtime?.toISOString(), + $filter: options?.$filter, + }, + { + allowReserved: options?.requestOptions?.skipUrlEncoding, + }, + ); return context - .path("/poolusagemetrics") - .get({ - ...operationOptionsToRequestParameters(options), - queryParameters: { - "api-version": options?.apiVersion ?? "2023-05-01.17.0", - maxresults: options?.maxresults, - timeOut: options?.timeOutInSeconds, - starttime: options?.starttime?.toISOString(), - endtime: options?.endtime?.toISOString(), - $filter: options?.$filter, - }, - }); + .path(path) + .get({ ...operationOptionsToRequestParameters(options) }); } export async function _listPoolUsageMetricsDeserialize( @@ -341,17 +355,23 @@ export function _createPoolSend( body: BatchPoolCreateOptions, options: CreatePoolOptionalParams = { requestOptions: {} }, ): StreamableMethod { + const path = expandUrlTemplate( + "/pools{?api-version,timeOut}", + { + "api-version": options?.apiVersion ?? "2023-05-01.17.0", + timeOut: options?.timeOutInSeconds, + }, + { + allowReserved: options?.requestOptions?.skipUrlEncoding, + }, + ); return context - .path("/pools") + .path(path) .post({ ...operationOptionsToRequestParameters(options), contentType: (options.contentType as any) ?? "application/json; odata=minimalmetadata", - queryParameters: { - "api-version": options?.apiVersion ?? "2023-05-01.17.0", - timeOut: options?.timeOutInSeconds, - }, body: batchPoolCreateOptionsSerializer(body), }); } @@ -385,9 +405,9 @@ export function _listPoolsSend( context: Client, options: ListPoolsOptionalParams = { requestOptions: {} }, ): StreamableMethod { - return context.path("/pools").get({ - ...operationOptionsToRequestParameters(options), - queryParameters: { + const path = expandUrlTemplate( + "/pools{?api-version,maxresults,timeOut,$filter,$select,$expand}", + { "api-version": options?.apiVersion ?? "2023-05-01.17.0", maxresults: options?.maxresults, timeOut: options?.timeOutInSeconds, @@ -403,7 +423,13 @@ export function _listPoolsSend( return p; }), }, - }); + { + allowReserved: options?.requestOptions?.skipUrlEncoding, + }, + ); + return context + .path(path) + .get({ ...operationOptionsToRequestParameters(options) }); } export async function _listPoolsDeserialize( @@ -436,8 +462,19 @@ export function _deletePoolSend( poolId: string, options: DeletePoolOptionalParams = { requestOptions: {} }, ): StreamableMethod { + const path = expandUrlTemplate( + "/pools/{poolId}{?api-version,timeOut}", + { + poolId: poolId, + "api-version": options?.apiVersion ?? "2023-05-01.17.0", + timeOut: options?.timeOutInSeconds, + }, + { + allowReserved: options?.requestOptions?.skipUrlEncoding, + }, + ); return context - .path("/pools/{poolId}", poolId) + .path(path) .delete({ ...operationOptionsToRequestParameters(options), headers: { @@ -462,10 +499,6 @@ export function _deletePoolSend( } : {}), }, - queryParameters: { - "api-version": options?.apiVersion ?? "2023-05-01.17.0", - timeOut: options?.timeOutInSeconds, - }, }); } @@ -508,8 +541,19 @@ export function _poolExistsSend( poolId: string, options: PoolExistsOptionalParams = { requestOptions: {} }, ): StreamableMethod { + const path = expandUrlTemplate( + "/pools/{poolId}{?api-version,timeOut}", + { + poolId: poolId, + "api-version": options?.apiVersion ?? "2023-05-01.17.0", + timeOut: options?.timeOutInSeconds, + }, + { + allowReserved: options?.requestOptions?.skipUrlEncoding, + }, + ); return context - .path("/pools/{poolId}", poolId) + .path(path) .head({ ...operationOptionsToRequestParameters(options), headers: { @@ -534,10 +578,6 @@ export function _poolExistsSend( } : {}), }, - queryParameters: { - "api-version": options?.apiVersion ?? "2023-05-01.17.0", - timeOut: options?.timeOutInSeconds, - }, }); } @@ -567,31 +607,10 @@ export function _getPoolSend( poolId: string, options: GetPoolOptionalParams = { requestOptions: {} }, ): StreamableMethod { - return context.path("/pools/{poolId}", poolId).get({ - ...operationOptionsToRequestParameters(options), - headers: { - ...(options?.ifMatch !== undefined - ? { "if-match": options?.ifMatch } - : {}), - ...(options?.ifNoneMatch !== undefined - ? { "if-none-match": options?.ifNoneMatch } - : {}), - ...(options?.ifModifiedSince !== undefined - ? { - "if-modified-since": !options?.ifModifiedSince - ? options?.ifModifiedSince - : options?.ifModifiedSince.toUTCString(), - } - : {}), - ...(options?.ifUnmodifiedSince !== undefined - ? { - "if-unmodified-since": !options?.ifUnmodifiedSince - ? options?.ifUnmodifiedSince - : options?.ifUnmodifiedSince.toUTCString(), - } - : {}), - }, - queryParameters: { + const path = expandUrlTemplate( + "/pools/{poolId}{?api-version,timeOut,$select,$expand}", + { + poolId: poolId, "api-version": options?.apiVersion ?? "2023-05-01.17.0", timeOut: options?.timeOutInSeconds, $select: !options?.$select @@ -605,7 +624,37 @@ export function _getPoolSend( return p; }), }, - }); + { + allowReserved: options?.requestOptions?.skipUrlEncoding, + }, + ); + return context + .path(path) + .get({ + ...operationOptionsToRequestParameters(options), + headers: { + ...(options?.ifMatch !== undefined + ? { "if-match": options?.ifMatch } + : {}), + ...(options?.ifNoneMatch !== undefined + ? { "if-none-match": options?.ifNoneMatch } + : {}), + ...(options?.ifModifiedSince !== undefined + ? { + "if-modified-since": !options?.ifModifiedSince + ? options?.ifModifiedSince + : options?.ifModifiedSince.toUTCString(), + } + : {}), + ...(options?.ifUnmodifiedSince !== undefined + ? { + "if-unmodified-since": !options?.ifUnmodifiedSince + ? options?.ifUnmodifiedSince + : options?.ifUnmodifiedSince.toUTCString(), + } + : {}), + }, + }); } export async function _getPoolDeserialize( @@ -635,8 +684,19 @@ export function _updatePoolSend( body: BatchPoolUpdateOptions, options: UpdatePoolOptionalParams = { requestOptions: {} }, ): StreamableMethod { + const path = expandUrlTemplate( + "/pools/{poolId}{?api-version,timeOut}", + { + poolId: poolId, + "api-version": options?.apiVersion ?? "2023-05-01.17.0", + timeOut: options?.timeOutInSeconds, + }, + { + allowReserved: options?.requestOptions?.skipUrlEncoding, + }, + ); return context - .path("/pools/{poolId}", poolId) + .path(path) .patch({ ...operationOptionsToRequestParameters(options), contentType: @@ -664,10 +724,6 @@ export function _updatePoolSend( } : {}), }, - queryParameters: { - "api-version": options?.apiVersion ?? "2023-05-01.17.0", - timeOut: options?.timeOutInSeconds, - }, body: batchPoolUpdateOptionsSerializer(body), }); } @@ -703,15 +759,20 @@ export function _disablePoolAutoScaleSend( poolId: string, options: DisablePoolAutoScaleOptionalParams = { requestOptions: {} }, ): StreamableMethod { + const path = expandUrlTemplate( + "/pools/{poolId}/disableautoscale{?api-version,timeOut}", + { + poolId: poolId, + "api-version": options?.apiVersion ?? "2023-05-01.17.0", + timeOut: options?.timeOutInSeconds, + }, + { + allowReserved: options?.requestOptions?.skipUrlEncoding, + }, + ); return context - .path("/pools/{poolId}/disableautoscale", poolId) - .post({ - ...operationOptionsToRequestParameters(options), - queryParameters: { - "api-version": options?.apiVersion ?? "2023-05-01.17.0", - timeOut: options?.timeOutInSeconds, - }, - }); + .path(path) + .post({ ...operationOptionsToRequestParameters(options) }); } export async function _disablePoolAutoScaleDeserialize( @@ -741,8 +802,19 @@ export function _enablePoolAutoScaleSend( body: BatchPoolEnableAutoScaleOptions, options: EnablePoolAutoScaleOptionalParams = { requestOptions: {} }, ): StreamableMethod { + const path = expandUrlTemplate( + "/pools/{poolId}/enableautoscale{?api-version,timeOut}", + { + poolId: poolId, + "api-version": options?.apiVersion ?? "2023-05-01.17.0", + timeOut: options?.timeOutInSeconds, + }, + { + allowReserved: options?.requestOptions?.skipUrlEncoding, + }, + ); return context - .path("/pools/{poolId}/enableautoscale", poolId) + .path(path) .post({ ...operationOptionsToRequestParameters(options), contentType: @@ -770,10 +842,6 @@ export function _enablePoolAutoScaleSend( } : {}), }, - queryParameters: { - "api-version": options?.apiVersion ?? "2023-05-01.17.0", - timeOut: options?.timeOutInSeconds, - }, body: batchPoolEnableAutoScaleOptionsSerializer(body), }); } @@ -813,17 +881,24 @@ export function _evaluatePoolAutoScaleSend( body: BatchPoolEvaluateAutoScaleOptions, options: EvaluatePoolAutoScaleOptionalParams = { requestOptions: {} }, ): StreamableMethod { + const path = expandUrlTemplate( + "/pools/{poolId}/evaluateautoscale{?api-version,timeOut}", + { + poolId: poolId, + "api-version": options?.apiVersion ?? "2023-05-01.17.0", + timeOut: options?.timeOutInSeconds, + }, + { + allowReserved: options?.requestOptions?.skipUrlEncoding, + }, + ); return context - .path("/pools/{poolId}/evaluateautoscale", poolId) + .path(path) .post({ ...operationOptionsToRequestParameters(options), contentType: (options.contentType as any) ?? "application/json; odata=minimalmetadata", - queryParameters: { - "api-version": options?.apiVersion ?? "2023-05-01.17.0", - timeOut: options?.timeOutInSeconds, - }, body: batchPoolEvaluateAutoScaleOptionsSerializer(body), }); } @@ -865,8 +940,19 @@ export function _resizePoolSend( body: BatchPoolResizeOptions, options: ResizePoolOptionalParams = { requestOptions: {} }, ): StreamableMethod { + const path = expandUrlTemplate( + "/pools/{poolId}/resize{?api-version,timeOut}", + { + poolId: poolId, + "api-version": options?.apiVersion ?? "2023-05-01.17.0", + timeOut: options?.timeOutInSeconds, + }, + { + allowReserved: options?.requestOptions?.skipUrlEncoding, + }, + ); return context - .path("/pools/{poolId}/resize", poolId) + .path(path) .post({ ...operationOptionsToRequestParameters(options), contentType: @@ -894,10 +980,6 @@ export function _resizePoolSend( } : {}), }, - queryParameters: { - "api-version": options?.apiVersion ?? "2023-05-01.17.0", - timeOut: options?.timeOutInSeconds, - }, body: batchPoolResizeOptionsSerializer(body), }); } @@ -937,8 +1019,19 @@ export function _stopPoolResizeSend( poolId: string, options: StopPoolResizeOptionalParams = { requestOptions: {} }, ): StreamableMethod { + const path = expandUrlTemplate( + "/pools/{poolId}/stopresize{?api-version,timeOut}", + { + poolId: poolId, + "api-version": options?.apiVersion ?? "2023-05-01.17.0", + timeOut: options?.timeOutInSeconds, + }, + { + allowReserved: options?.requestOptions?.skipUrlEncoding, + }, + ); return context - .path("/pools/{poolId}/stopresize", poolId) + .path(path) .post({ ...operationOptionsToRequestParameters(options), headers: { @@ -963,10 +1056,6 @@ export function _stopPoolResizeSend( } : {}), }, - queryParameters: { - "api-version": options?.apiVersion ?? "2023-05-01.17.0", - timeOut: options?.timeOutInSeconds, - }, }); } @@ -1005,17 +1094,24 @@ export function _replacePoolPropertiesSend( body: BatchPoolReplaceOptions, options: ReplacePoolPropertiesOptionalParams = { requestOptions: {} }, ): StreamableMethod { + const path = expandUrlTemplate( + "/pools/{poolId}/updateproperties{?api-version,timeOut}", + { + poolId: poolId, + "api-version": options?.apiVersion ?? "2023-05-01.17.0", + timeOut: options?.timeOutInSeconds, + }, + { + allowReserved: options?.requestOptions?.skipUrlEncoding, + }, + ); return context - .path("/pools/{poolId}/updateproperties", poolId) + .path(path) .post({ ...operationOptionsToRequestParameters(options), contentType: (options.contentType as any) ?? "application/json; odata=minimalmetadata", - queryParameters: { - "api-version": options?.apiVersion ?? "2023-05-01.17.0", - timeOut: options?.timeOutInSeconds, - }, body: batchPoolReplaceOptionsSerializer(body), }); } @@ -1057,8 +1153,19 @@ export function _removeNodesSend( body: NodeRemoveOptions, options: RemoveNodesOptionalParams = { requestOptions: {} }, ): StreamableMethod { + const path = expandUrlTemplate( + "/pools/{poolId}/removenodes{?api-version,timeOut}", + { + poolId: poolId, + "api-version": options?.apiVersion ?? "2023-05-01.17.0", + timeOut: options?.timeOutInSeconds, + }, + { + allowReserved: options?.requestOptions?.skipUrlEncoding, + }, + ); return context - .path("/pools/{poolId}/removenodes", poolId) + .path(path) .post({ ...operationOptionsToRequestParameters(options), contentType: @@ -1086,10 +1193,6 @@ export function _removeNodesSend( } : {}), }, - queryParameters: { - "api-version": options?.apiVersion ?? "2023-05-01.17.0", - timeOut: options?.timeOutInSeconds, - }, body: nodeRemoveOptionsSerializer(body), }); } @@ -1124,16 +1227,20 @@ export function _listSupportedImagesSend( context: Client, options: ListSupportedImagesOptionalParams = { requestOptions: {} }, ): StreamableMethod { + const path = expandUrlTemplate( + "/supportedimages{?maxresults,timeOut,$filter}", + { + maxresults: options?.maxresults, + timeOut: options?.timeOutInSeconds, + $filter: options?.$filter, + }, + { + allowReserved: options?.requestOptions?.skipUrlEncoding, + }, + ); return context - .path("/supportedimages") - .get({ - ...operationOptionsToRequestParameters(options), - queryParameters: { - maxresults: options?.maxresults, - timeOut: options?.timeOutInSeconds, - $filter: options?.$filter, - }, - }); + .path(path) + .get({ ...operationOptionsToRequestParameters(options) }); } export async function _listSupportedImagesDeserialize( @@ -1165,17 +1272,21 @@ export function _listPoolNodeCountsSend( context: Client, options: ListPoolNodeCountsOptionalParams = { requestOptions: {} }, ): StreamableMethod { + const path = expandUrlTemplate( + "/nodecounts{?api-version,maxresults,timeOut,$filter}", + { + "api-version": options?.apiVersion ?? "2023-05-01.17.0", + maxresults: options?.maxresults, + timeOut: options?.timeOutInSeconds, + $filter: options?.$filter, + }, + { + allowReserved: options?.requestOptions?.skipUrlEncoding, + }, + ); return context - .path("/nodecounts") - .get({ - ...operationOptionsToRequestParameters(options), - queryParameters: { - "api-version": options?.apiVersion ?? "2023-05-01.17.0", - maxresults: options?.maxresults, - timeOut: options?.timeOutInSeconds, - $filter: options?.$filter, - }, - }); + .path(path) + .get({ ...operationOptionsToRequestParameters(options) }); } export async function _listPoolNodeCountsDeserialize( @@ -1212,8 +1323,19 @@ export function _deleteJobSend( jobId: string, options: DeleteJobOptionalParams = { requestOptions: {} }, ): StreamableMethod { + const path = expandUrlTemplate( + "/jobs/{jobId}{?api-version,timeOut}", + { + jobId: jobId, + "api-version": options?.apiVersion ?? "2023-05-01.17.0", + timeOut: options?.timeOutInSeconds, + }, + { + allowReserved: options?.requestOptions?.skipUrlEncoding, + }, + ); return context - .path("/jobs/{jobId}", jobId) + .path(path) .delete({ ...operationOptionsToRequestParameters(options), headers: { @@ -1238,10 +1360,6 @@ export function _deleteJobSend( } : {}), }, - queryParameters: { - "api-version": options?.apiVersion ?? "2023-05-01.17.0", - timeOut: options?.timeOutInSeconds, - }, }); } @@ -1280,31 +1398,10 @@ export function _getJobSend( jobId: string, options: GetJobOptionalParams = { requestOptions: {} }, ): StreamableMethod { - return context.path("/jobs/{jobId}", jobId).get({ - ...operationOptionsToRequestParameters(options), - headers: { - ...(options?.ifMatch !== undefined - ? { "if-match": options?.ifMatch } - : {}), - ...(options?.ifNoneMatch !== undefined - ? { "if-none-match": options?.ifNoneMatch } - : {}), - ...(options?.ifModifiedSince !== undefined - ? { - "if-modified-since": !options?.ifModifiedSince - ? options?.ifModifiedSince - : options?.ifModifiedSince.toUTCString(), - } - : {}), - ...(options?.ifUnmodifiedSince !== undefined - ? { - "if-unmodified-since": !options?.ifUnmodifiedSince - ? options?.ifUnmodifiedSince - : options?.ifUnmodifiedSince.toUTCString(), - } - : {}), - }, - queryParameters: { + const path = expandUrlTemplate( + "/jobs/{jobId}{?api-version,timeOut,$select,$expand}", + { + jobId: jobId, "api-version": options?.apiVersion ?? "2023-05-01.17.0", timeOut: options?.timeOutInSeconds, $select: !options?.$select @@ -1318,7 +1415,37 @@ export function _getJobSend( return p; }), }, - }); + { + allowReserved: options?.requestOptions?.skipUrlEncoding, + }, + ); + return context + .path(path) + .get({ + ...operationOptionsToRequestParameters(options), + headers: { + ...(options?.ifMatch !== undefined + ? { "if-match": options?.ifMatch } + : {}), + ...(options?.ifNoneMatch !== undefined + ? { "if-none-match": options?.ifNoneMatch } + : {}), + ...(options?.ifModifiedSince !== undefined + ? { + "if-modified-since": !options?.ifModifiedSince + ? options?.ifModifiedSince + : options?.ifModifiedSince.toUTCString(), + } + : {}), + ...(options?.ifUnmodifiedSince !== undefined + ? { + "if-unmodified-since": !options?.ifUnmodifiedSince + ? options?.ifUnmodifiedSince + : options?.ifUnmodifiedSince.toUTCString(), + } + : {}), + }, + }); } export async function _getJobDeserialize( @@ -1348,8 +1475,19 @@ export function _updateJobSend( body: BatchJobUpdateOptions, options: UpdateJobOptionalParams = { requestOptions: {} }, ): StreamableMethod { + const path = expandUrlTemplate( + "/jobs/{jobId}{?api-version,timeOut}", + { + jobId: jobId, + "api-version": options?.apiVersion ?? "2023-05-01.17.0", + timeOut: options?.timeOutInSeconds, + }, + { + allowReserved: options?.requestOptions?.skipUrlEncoding, + }, + ); return context - .path("/jobs/{jobId}", jobId) + .path(path) .patch({ ...operationOptionsToRequestParameters(options), contentType: @@ -1377,10 +1515,6 @@ export function _updateJobSend( } : {}), }, - queryParameters: { - "api-version": options?.apiVersion ?? "2023-05-01.17.0", - timeOut: options?.timeOutInSeconds, - }, body: batchJobUpdateOptionsSerializer(body), }); } @@ -1417,8 +1551,19 @@ export function _replaceJobSend( body: BatchJob, options: ReplaceJobOptionalParams = { requestOptions: {} }, ): StreamableMethod { + const path = expandUrlTemplate( + "/jobs/{jobId}{?api-version,timeOut}", + { + jobId: jobId, + "api-version": options?.apiVersion ?? "2023-05-01.17.0", + timeOut: options?.timeOutInSeconds, + }, + { + allowReserved: options?.requestOptions?.skipUrlEncoding, + }, + ); return context - .path("/jobs/{jobId}", jobId) + .path(path) .put({ ...operationOptionsToRequestParameters(options), contentType: @@ -1446,10 +1591,6 @@ export function _replaceJobSend( } : {}), }, - queryParameters: { - "api-version": options?.apiVersion ?? "2023-05-01.17.0", - timeOut: options?.timeOutInSeconds, - }, body: batchJobSerializer(body), }); } @@ -1486,8 +1627,19 @@ export function _disableJobSend( body: BatchJobDisableOptions, options: DisableJobOptionalParams = { requestOptions: {} }, ): StreamableMethod { + const path = expandUrlTemplate( + "/jobs/{jobId}/disable{?api-version,timeOut}", + { + jobId: jobId, + "api-version": options?.apiVersion ?? "2023-05-01.17.0", + timeOut: options?.timeOutInSeconds, + }, + { + allowReserved: options?.requestOptions?.skipUrlEncoding, + }, + ); return context - .path("/jobs/{jobId}/disable", jobId) + .path(path) .post({ ...operationOptionsToRequestParameters(options), contentType: @@ -1515,10 +1667,6 @@ export function _disableJobSend( } : {}), }, - queryParameters: { - "api-version": options?.apiVersion ?? "2023-05-01.17.0", - timeOut: options?.timeOutInSeconds, - }, body: batchJobDisableOptionsSerializer(body), }); } @@ -1559,8 +1707,19 @@ export function _enableJobSend( jobId: string, options: EnableJobOptionalParams = { requestOptions: {} }, ): StreamableMethod { + const path = expandUrlTemplate( + "/jobs/{jobId}/enable{?api-version,timeOut}", + { + jobId: jobId, + "api-version": options?.apiVersion ?? "2023-05-01.17.0", + timeOut: options?.timeOutInSeconds, + }, + { + allowReserved: options?.requestOptions?.skipUrlEncoding, + }, + ); return context - .path("/jobs/{jobId}/enable", jobId) + .path(path) .post({ ...operationOptionsToRequestParameters(options), headers: { @@ -1585,10 +1744,6 @@ export function _enableJobSend( } : {}), }, - queryParameters: { - "api-version": options?.apiVersion ?? "2023-05-01.17.0", - timeOut: options?.timeOutInSeconds, - }, }); } @@ -1625,8 +1780,19 @@ export function _terminateJobSend( jobId: string, options: TerminateJobOptionalParams = { requestOptions: {} }, ): StreamableMethod { + const path = expandUrlTemplate( + "/jobs/{jobId}/terminate{?api-version,timeOut}", + { + jobId: jobId, + "api-version": options?.apiVersion ?? "2023-05-01.17.0", + timeOut: options?.timeOutInSeconds, + }, + { + allowReserved: options?.requestOptions?.skipUrlEncoding, + }, + ); return context - .path("/jobs/{jobId}/terminate", jobId) + .path(path) .post({ ...operationOptionsToRequestParameters(options), contentType: @@ -1654,10 +1820,6 @@ export function _terminateJobSend( } : {}), }, - queryParameters: { - "api-version": options?.apiVersion ?? "2023-05-01.17.0", - timeOut: options?.timeOutInSeconds, - }, body: !options["body"] ? options["body"] : batchJobTerminateOptionsSerializer(options["body"]), @@ -1697,17 +1859,23 @@ export function _createJobSend( body: BatchJobCreateOptions, options: CreateJobOptionalParams = { requestOptions: {} }, ): StreamableMethod { + const path = expandUrlTemplate( + "/jobs{?api-version,timeOut}", + { + "api-version": options?.apiVersion ?? "2023-05-01.17.0", + timeOut: options?.timeOutInSeconds, + }, + { + allowReserved: options?.requestOptions?.skipUrlEncoding, + }, + ); return context - .path("/jobs") + .path(path) .post({ ...operationOptionsToRequestParameters(options), contentType: (options.contentType as any) ?? "application/json; odata=minimalmetadata", - queryParameters: { - "api-version": options?.apiVersion ?? "2023-05-01.17.0", - timeOut: options?.timeOutInSeconds, - }, body: batchJobCreateOptionsSerializer(body), }); } @@ -1747,9 +1915,9 @@ export function _listJobsSend( context: Client, options: ListJobsOptionalParams = { requestOptions: {} }, ): StreamableMethod { - return context.path("/jobs").get({ - ...operationOptionsToRequestParameters(options), - queryParameters: { + const path = expandUrlTemplate( + "/jobs{?api-version,maxresults,timeOut,$filter,$select,$expand}", + { "api-version": options?.apiVersion ?? "2023-05-01.17.0", maxresults: options?.maxresults, timeOut: options?.timeOutInSeconds, @@ -1765,7 +1933,13 @@ export function _listJobsSend( return p; }), }, - }); + { + allowReserved: options?.requestOptions?.skipUrlEncoding, + }, + ); + return context + .path(path) + .get({ ...operationOptionsToRequestParameters(options) }); } export async function _listJobsDeserialize( @@ -1798,9 +1972,10 @@ export function _listJobsFromScheduleSend( jobScheduleId: string, options: ListJobsFromScheduleOptionalParams = { requestOptions: {} }, ): StreamableMethod { - return context.path("/jobschedules/{jobScheduleId}/jobs", jobScheduleId).get({ - ...operationOptionsToRequestParameters(options), - queryParameters: { + const path = expandUrlTemplate( + "/jobschedules/{jobScheduleId}/jobs{?api-version,maxresults,timeOut,$filter,$select,$expand}", + { + jobScheduleId: jobScheduleId, "api-version": options?.apiVersion ?? "2023-05-01.17.0", maxresults: options?.maxresults, timeOut: options?.timeOutInSeconds, @@ -1816,7 +1991,13 @@ export function _listJobsFromScheduleSend( return p; }), }, - }); + { + allowReserved: options?.requestOptions?.skipUrlEncoding, + }, + ); + return context + .path(path) + .get({ ...operationOptionsToRequestParameters(options) }); } export async function _listJobsFromScheduleDeserialize( @@ -1852,21 +2033,26 @@ export function _listJobPreparationAndReleaseTaskStatusSend( requestOptions: {}, }, ): StreamableMethod { + const path = expandUrlTemplate( + "/jobs/{jobId}/jobpreparationandreleasetaskstatus{?maxresults,timeOut,$filter,$select}", + { + jobId: jobId, + maxresults: options?.maxresults, + timeOut: options?.timeOutInSeconds, + $filter: options?.$filter, + $select: !options?.$select + ? options?.$select + : options?.$select.map((p: any) => { + return p; + }), + }, + { + allowReserved: options?.requestOptions?.skipUrlEncoding, + }, + ); return context - .path("/jobs/{jobId}/jobpreparationandreleasetaskstatus", jobId) - .get({ - ...operationOptionsToRequestParameters(options), - queryParameters: { - maxresults: options?.maxresults, - timeOut: options?.timeOutInSeconds, - $filter: options?.$filter, - $select: !options?.$select - ? options?.$select - : options?.$select.map((p: any) => { - return p; - }), - }, - }); + .path(path) + .get({ ...operationOptionsToRequestParameters(options) }); } export async function _listJobPreparationAndReleaseTaskStatusDeserialize( @@ -1911,15 +2097,20 @@ export function _getJobTaskCountsSend( jobId: string, options: GetJobTaskCountsOptionalParams = { requestOptions: {} }, ): StreamableMethod { + const path = expandUrlTemplate( + "/jobs/{jobId}/taskcounts{?api-version,timeOut}", + { + jobId: jobId, + "api-version": options?.apiVersion ?? "2023-05-01.17.0", + timeOut: options?.timeOutInSeconds, + }, + { + allowReserved: options?.requestOptions?.skipUrlEncoding, + }, + ); return context - .path("/jobs/{jobId}/taskcounts", jobId) - .get({ - ...operationOptionsToRequestParameters(options), - queryParameters: { - "api-version": options?.apiVersion ?? "2023-05-01.17.0", - timeOut: options?.timeOutInSeconds, - }, - }); + .path(path) + .get({ ...operationOptionsToRequestParameters(options) }); } export async function _getJobTaskCountsDeserialize( @@ -1953,17 +2144,23 @@ export function _createCertificateSend( body: BatchCertificate, options: CreateCertificateOptionalParams = { requestOptions: {} }, ): StreamableMethod { + const path = expandUrlTemplate( + "/certificates{?api-version,timeOut}", + { + "api-version": options?.apiVersion ?? "2023-05-01.17.0", + timeOut: options?.timeOutInSeconds, + }, + { + allowReserved: options?.requestOptions?.skipUrlEncoding, + }, + ); return context - .path("/certificates") + .path(path) .post({ ...operationOptionsToRequestParameters(options), contentType: (options.contentType as any) ?? "application/json; odata=minimalmetadata", - queryParameters: { - "api-version": options?.apiVersion ?? "2023-05-01.17.0", - timeOut: options?.timeOutInSeconds, - }, body: batchCertificateSerializer(body), }); } @@ -1993,9 +2190,9 @@ export function _listCertificatesSend( context: Client, options: ListCertificatesOptionalParams = { requestOptions: {} }, ): StreamableMethod { - return context.path("/certificates").get({ - ...operationOptionsToRequestParameters(options), - queryParameters: { + const path = expandUrlTemplate( + "/certificates{?api-version,maxresults,timeOut,$filter,$select}", + { "api-version": options?.apiVersion ?? "2023-05-01.17.0", maxresults: options?.maxresults, timeOut: options?.timeOutInSeconds, @@ -2006,7 +2203,13 @@ export function _listCertificatesSend( return p; }), }, - }); + { + allowReserved: options?.requestOptions?.skipUrlEncoding, + }, + ); + return context + .path(path) + .get({ ...operationOptionsToRequestParameters(options) }); } export async function _listCertificatesDeserialize( @@ -2040,19 +2243,21 @@ export function _cancelCertificateDeletionSend( thumbprint: string, options: CancelCertificateDeletionOptionalParams = { requestOptions: {} }, ): StreamableMethod { + const path = expandUrlTemplate( + "/certificates(thumbprintAlgorithm={thumbprintAlgorithm},thumbprint={thumbprint})/canceldelete{?api-version,timeOut}", + { + thumbprintAlgorithm: thumbprintAlgorithm, + thumbprint: thumbprint, + "api-version": options?.apiVersion ?? "2023-05-01.17.0", + timeOut: options?.timeOutInSeconds, + }, + { + allowReserved: options?.requestOptions?.skipUrlEncoding, + }, + ); return context - .path( - "/certificates(thumbprintAlgorithm={thumbprintAlgorithm},thumbprint={thumbprint})/canceldelete", - thumbprintAlgorithm, - thumbprint, - ) - .post({ - ...operationOptionsToRequestParameters(options), - queryParameters: { - "api-version": options?.apiVersion ?? "2023-05-01.17.0", - timeOut: options?.timeOutInSeconds, - }, - }); + .path(path) + .post({ ...operationOptionsToRequestParameters(options) }); } export async function _cancelCertificateDeletionDeserialize( @@ -2096,19 +2301,21 @@ export function _deleteCertificateSend( thumbprint: string, options: DeleteCertificateOptionalParams = { requestOptions: {} }, ): StreamableMethod { + const path = expandUrlTemplate( + "/certificates(thumbprintAlgorithm={thumbprintAlgorithm},thumbprint={thumbprint}){?api-version,timeOut}", + { + thumbprintAlgorithm: thumbprintAlgorithm, + thumbprint: thumbprint, + "api-version": options?.apiVersion ?? "2023-05-01.17.0", + timeOut: options?.timeOutInSeconds, + }, + { + allowReserved: options?.requestOptions?.skipUrlEncoding, + }, + ); return context - .path( - "/certificates(thumbprintAlgorithm={thumbprintAlgorithm},thumbprint={thumbprint})", - thumbprintAlgorithm, - thumbprint, - ) - .delete({ - ...operationOptionsToRequestParameters(options), - queryParameters: { - "api-version": options?.apiVersion ?? "2023-05-01.17.0", - timeOut: options?.timeOutInSeconds, - }, - }); + .path(path) + .delete({ ...operationOptionsToRequestParameters(options) }); } export async function _deleteCertificateDeserialize( @@ -2154,24 +2361,26 @@ export function _getCertificateSend( thumbprint: string, options: GetCertificateOptionalParams = { requestOptions: {} }, ): StreamableMethod { + const path = expandUrlTemplate( + "/certificates(thumbprintAlgorithm={thumbprintAlgorithm},thumbprint={thumbprint}){?api-version,timeOut,$select}", + { + thumbprintAlgorithm: thumbprintAlgorithm, + thumbprint: thumbprint, + "api-version": options?.apiVersion ?? "2023-05-01.17.0", + timeOut: options?.timeOutInSeconds, + $select: !options?.$select + ? options?.$select + : options?.$select.map((p: any) => { + return p; + }), + }, + { + allowReserved: options?.requestOptions?.skipUrlEncoding, + }, + ); return context - .path( - "/certificates(thumbprintAlgorithm={thumbprintAlgorithm},thumbprint={thumbprint})", - thumbprintAlgorithm, - thumbprint, - ) - .get({ - ...operationOptionsToRequestParameters(options), - queryParameters: { - "api-version": options?.apiVersion ?? "2023-05-01.17.0", - timeOut: options?.timeOutInSeconds, - $select: !options?.$select - ? options?.$select - : options?.$select.map((p: any) => { - return p; - }), - }, - }); + .path(path) + .get({ ...operationOptionsToRequestParameters(options) }); } export async function _getCertificateDeserialize( @@ -2206,8 +2415,19 @@ export function _jobScheduleExistsSend( jobScheduleId: string, options: JobScheduleExistsOptionalParams = { requestOptions: {} }, ): StreamableMethod { + const path = expandUrlTemplate( + "/jobschedules/{jobScheduleId}{?api-version,timeOut}", + { + jobScheduleId: jobScheduleId, + "api-version": options?.apiVersion ?? "2023-05-01.17.0", + timeOut: options?.timeOutInSeconds, + }, + { + allowReserved: options?.requestOptions?.skipUrlEncoding, + }, + ); return context - .path("/jobschedules/{jobScheduleId}", jobScheduleId) + .path(path) .head({ ...operationOptionsToRequestParameters(options), headers: { @@ -2232,10 +2452,6 @@ export function _jobScheduleExistsSend( } : {}), }, - queryParameters: { - "api-version": options?.apiVersion ?? "2023-05-01.17.0", - timeOut: options?.timeOutInSeconds, - }, }); } @@ -2265,8 +2481,19 @@ export function _deleteJobScheduleSend( jobScheduleId: string, options: DeleteJobScheduleOptionalParams = { requestOptions: {} }, ): StreamableMethod { + const path = expandUrlTemplate( + "/jobschedules/{jobScheduleId}{?api-version,timeOut}", + { + jobScheduleId: jobScheduleId, + "api-version": options?.apiVersion ?? "2023-05-01.17.0", + timeOut: options?.timeOutInSeconds, + }, + { + allowReserved: options?.requestOptions?.skipUrlEncoding, + }, + ); return context - .path("/jobschedules/{jobScheduleId}", jobScheduleId) + .path(path) .delete({ ...operationOptionsToRequestParameters(options), headers: { @@ -2291,10 +2518,6 @@ export function _deleteJobScheduleSend( } : {}), }, - queryParameters: { - "api-version": options?.apiVersion ?? "2023-05-01.17.0", - timeOut: options?.timeOutInSeconds, - }, }); } @@ -2330,31 +2553,10 @@ export function _getJobScheduleSend( jobScheduleId: string, options: GetJobScheduleOptionalParams = { requestOptions: {} }, ): StreamableMethod { - return context.path("/jobschedules/{jobScheduleId}", jobScheduleId).get({ - ...operationOptionsToRequestParameters(options), - headers: { - ...(options?.ifMatch !== undefined - ? { "if-match": options?.ifMatch } - : {}), - ...(options?.ifNoneMatch !== undefined - ? { "if-none-match": options?.ifNoneMatch } - : {}), - ...(options?.ifModifiedSince !== undefined - ? { - "if-modified-since": !options?.ifModifiedSince - ? options?.ifModifiedSince - : options?.ifModifiedSince.toUTCString(), - } - : {}), - ...(options?.ifUnmodifiedSince !== undefined - ? { - "if-unmodified-since": !options?.ifUnmodifiedSince - ? options?.ifUnmodifiedSince - : options?.ifUnmodifiedSince.toUTCString(), - } - : {}), - }, - queryParameters: { + const path = expandUrlTemplate( + "/jobschedules/{jobScheduleId}{?api-version,timeOut,$select,$expand}", + { + jobScheduleId: jobScheduleId, "api-version": options?.apiVersion ?? "2023-05-01.17.0", timeOut: options?.timeOutInSeconds, $select: !options?.$select @@ -2368,7 +2570,37 @@ export function _getJobScheduleSend( return p; }), }, - }); + { + allowReserved: options?.requestOptions?.skipUrlEncoding, + }, + ); + return context + .path(path) + .get({ + ...operationOptionsToRequestParameters(options), + headers: { + ...(options?.ifMatch !== undefined + ? { "if-match": options?.ifMatch } + : {}), + ...(options?.ifNoneMatch !== undefined + ? { "if-none-match": options?.ifNoneMatch } + : {}), + ...(options?.ifModifiedSince !== undefined + ? { + "if-modified-since": !options?.ifModifiedSince + ? options?.ifModifiedSince + : options?.ifModifiedSince.toUTCString(), + } + : {}), + ...(options?.ifUnmodifiedSince !== undefined + ? { + "if-unmodified-since": !options?.ifUnmodifiedSince + ? options?.ifUnmodifiedSince + : options?.ifUnmodifiedSince.toUTCString(), + } + : {}), + }, + }); } export async function _getJobScheduleDeserialize( @@ -2398,8 +2630,19 @@ export function _updateJobScheduleSend( body: BatchJobScheduleUpdateOptions, options: UpdateJobScheduleOptionalParams = { requestOptions: {} }, ): StreamableMethod { + const path = expandUrlTemplate( + "/jobschedules/{jobScheduleId}{?api-version,timeOut}", + { + jobScheduleId: jobScheduleId, + "api-version": options?.apiVersion ?? "2023-05-01.17.0", + timeOut: options?.timeOutInSeconds, + }, + { + allowReserved: options?.requestOptions?.skipUrlEncoding, + }, + ); return context - .path("/jobschedules/{jobScheduleId}", jobScheduleId) + .path(path) .patch({ ...operationOptionsToRequestParameters(options), contentType: @@ -2427,10 +2670,6 @@ export function _updateJobScheduleSend( } : {}), }, - queryParameters: { - "api-version": options?.apiVersion ?? "2023-05-01.17.0", - timeOut: options?.timeOutInSeconds, - }, body: batchJobScheduleUpdateOptionsSerializer(body), }); } @@ -2474,8 +2713,19 @@ export function _replaceJobScheduleSend( body: BatchJobSchedule, options: ReplaceJobScheduleOptionalParams = { requestOptions: {} }, ): StreamableMethod { + const path = expandUrlTemplate( + "/jobschedules/{jobScheduleId}{?api-version,timeOut}", + { + jobScheduleId: jobScheduleId, + "api-version": options?.apiVersion ?? "2023-05-01.17.0", + timeOut: options?.timeOutInSeconds, + }, + { + allowReserved: options?.requestOptions?.skipUrlEncoding, + }, + ); return context - .path("/jobschedules/{jobScheduleId}", jobScheduleId) + .path(path) .put({ ...operationOptionsToRequestParameters(options), contentType: @@ -2503,10 +2753,6 @@ export function _replaceJobScheduleSend( } : {}), }, - queryParameters: { - "api-version": options?.apiVersion ?? "2023-05-01.17.0", - timeOut: options?.timeOutInSeconds, - }, body: batchJobScheduleSerializer(body), }); } @@ -2549,8 +2795,19 @@ export function _disableJobScheduleSend( jobScheduleId: string, options: DisableJobScheduleOptionalParams = { requestOptions: {} }, ): StreamableMethod { + const path = expandUrlTemplate( + "/jobschedules/{jobScheduleId}/disable{?api-version,timeOut}", + { + jobScheduleId: jobScheduleId, + "api-version": options?.apiVersion ?? "2023-05-01.17.0", + timeOut: options?.timeOutInSeconds, + }, + { + allowReserved: options?.requestOptions?.skipUrlEncoding, + }, + ); return context - .path("/jobschedules/{jobScheduleId}/disable", jobScheduleId) + .path(path) .post({ ...operationOptionsToRequestParameters(options), headers: { @@ -2575,10 +2832,6 @@ export function _disableJobScheduleSend( } : {}), }, - queryParameters: { - "api-version": options?.apiVersion ?? "2023-05-01.17.0", - timeOut: options?.timeOutInSeconds, - }, }); } @@ -2608,8 +2861,19 @@ export function _enableJobScheduleSend( jobScheduleId: string, options: EnableJobScheduleOptionalParams = { requestOptions: {} }, ): StreamableMethod { + const path = expandUrlTemplate( + "/jobschedules/{jobScheduleId}/enable{?api-version,timeOut}", + { + jobScheduleId: jobScheduleId, + "api-version": options?.apiVersion ?? "2023-05-01.17.0", + timeOut: options?.timeOutInSeconds, + }, + { + allowReserved: options?.requestOptions?.skipUrlEncoding, + }, + ); return context - .path("/jobschedules/{jobScheduleId}/enable", jobScheduleId) + .path(path) .post({ ...operationOptionsToRequestParameters(options), headers: { @@ -2634,10 +2898,6 @@ export function _enableJobScheduleSend( } : {}), }, - queryParameters: { - "api-version": options?.apiVersion ?? "2023-05-01.17.0", - timeOut: options?.timeOutInSeconds, - }, }); } @@ -2667,8 +2927,19 @@ export function _terminateJobScheduleSend( jobScheduleId: string, options: TerminateJobScheduleOptionalParams = { requestOptions: {} }, ): StreamableMethod { + const path = expandUrlTemplate( + "/jobschedules/{jobScheduleId}/terminate{?api-version,timeOut}", + { + jobScheduleId: jobScheduleId, + "api-version": options?.apiVersion ?? "2023-05-01.17.0", + timeOut: options?.timeOutInSeconds, + }, + { + allowReserved: options?.requestOptions?.skipUrlEncoding, + }, + ); return context - .path("/jobschedules/{jobScheduleId}/terminate", jobScheduleId) + .path(path) .post({ ...operationOptionsToRequestParameters(options), headers: { @@ -2693,10 +2964,6 @@ export function _terminateJobScheduleSend( } : {}), }, - queryParameters: { - "api-version": options?.apiVersion ?? "2023-05-01.17.0", - timeOut: options?.timeOutInSeconds, - }, }); } @@ -2730,17 +2997,23 @@ export function _createJobScheduleSend( body: BatchJobScheduleCreateOptions, options: CreateJobScheduleOptionalParams = { requestOptions: {} }, ): StreamableMethod { + const path = expandUrlTemplate( + "/jobschedules{?api-version,timeOut}", + { + "api-version": options?.apiVersion ?? "2023-05-01.17.0", + timeOut: options?.timeOutInSeconds, + }, + { + allowReserved: options?.requestOptions?.skipUrlEncoding, + }, + ); return context - .path("/jobschedules") + .path(path) .post({ ...operationOptionsToRequestParameters(options), contentType: (options.contentType as any) ?? "application/json; odata=minimalmetadata", - queryParameters: { - "api-version": options?.apiVersion ?? "2023-05-01.17.0", - timeOut: options?.timeOutInSeconds, - }, body: batchJobScheduleCreateOptionsSerializer(body), }); } @@ -2770,9 +3043,9 @@ export function _listJobSchedulesSend( context: Client, options: ListJobSchedulesOptionalParams = { requestOptions: {} }, ): StreamableMethod { - return context.path("/jobschedules").get({ - ...operationOptionsToRequestParameters(options), - queryParameters: { + const path = expandUrlTemplate( + "/jobschedules{?api-version,maxresults,timeOut,$filter,$select,$expand}", + { "api-version": options?.apiVersion ?? "2023-05-01.17.0", maxresults: options?.maxresults, timeOut: options?.timeOutInSeconds, @@ -2788,7 +3061,13 @@ export function _listJobSchedulesSend( return p; }), }, - }); + { + allowReserved: options?.requestOptions?.skipUrlEncoding, + }, + ); + return context + .path(path) + .get({ ...operationOptionsToRequestParameters(options) }); } export async function _listJobSchedulesDeserialize( @@ -2822,17 +3101,24 @@ export function _createTaskSend( body: BatchTaskCreateOptions, options: CreateTaskOptionalParams = { requestOptions: {} }, ): StreamableMethod { + const path = expandUrlTemplate( + "/jobs/{jobId}/tasks{?api-version,timeOut}", + { + jobId: jobId, + "api-version": options?.apiVersion ?? "2023-05-01.17.0", + timeOut: options?.timeOutInSeconds, + }, + { + allowReserved: options?.requestOptions?.skipUrlEncoding, + }, + ); return context - .path("/jobs/{jobId}/tasks", jobId) + .path(path) .post({ ...operationOptionsToRequestParameters(options), contentType: (options.contentType as any) ?? "application/json; odata=minimalmetadata", - queryParameters: { - "api-version": options?.apiVersion ?? "2023-05-01.17.0", - timeOut: options?.timeOutInSeconds, - }, body: batchTaskCreateOptionsSerializer(body), }); } @@ -2868,9 +3154,10 @@ export function _listTasksSend( jobId: string, options: ListTasksOptionalParams = { requestOptions: {} }, ): StreamableMethod { - return context.path("/jobs/{jobId}/tasks", jobId).get({ - ...operationOptionsToRequestParameters(options), - queryParameters: { + const path = expandUrlTemplate( + "/jobs/{jobId}/tasks{?api-version,maxresults,timeOut,$filter,$select,$expand}", + { + jobId: jobId, "api-version": options?.apiVersion ?? "2023-05-01.17.0", maxresults: options?.maxresults, timeOut: options?.timeOutInSeconds, @@ -2886,7 +3173,13 @@ export function _listTasksSend( return p; }), }, - }); + { + allowReserved: options?.requestOptions?.skipUrlEncoding, + }, + ); + return context + .path(path) + .get({ ...operationOptionsToRequestParameters(options) }); } export async function _listTasksDeserialize( @@ -2925,17 +3218,24 @@ export function _createTaskCollectionSend( collection: BatchTaskCollection, options: CreateTaskCollectionOptionalParams = { requestOptions: {} }, ): StreamableMethod { + const path = expandUrlTemplate( + "/jobs/{jobId}/addtaskcollection{?api-version,timeOut}", + { + jobId: jobId, + "api-version": options?.apiVersion ?? "2023-05-01.17.0", + timeOut: options?.timeOutInSeconds, + }, + { + allowReserved: options?.requestOptions?.skipUrlEncoding, + }, + ); return context - .path("/jobs/{jobId}/addtaskcollection", jobId) + .path(path) .post({ ...operationOptionsToRequestParameters(options), contentType: (options.contentType as any) ?? "application/json; odata=minimalmetadata", - queryParameters: { - "api-version": options?.apiVersion ?? "2023-05-01.17.0", - timeOut: options?.timeOutInSeconds, - }, body: batchTaskCollectionSerializer(collection), }); } @@ -2988,8 +3288,20 @@ export function _deleteTaskSend( taskId: string, options: DeleteTaskOptionalParams = { requestOptions: {} }, ): StreamableMethod { + const path = expandUrlTemplate( + "/jobs/{jobId}/tasks/{taskId}{?api-version,timeOut}", + { + jobId: jobId, + taskId: taskId, + "api-version": options?.apiVersion ?? "2023-05-01.17.0", + timeOut: options?.timeOutInSeconds, + }, + { + allowReserved: options?.requestOptions?.skipUrlEncoding, + }, + ); return context - .path("/jobs/{jobId}/tasks/{taskId}", jobId, taskId) + .path(path) .delete({ ...operationOptionsToRequestParameters(options), headers: { @@ -3014,10 +3326,6 @@ export function _deleteTaskSend( } : {}), }, - queryParameters: { - "api-version": options?.apiVersion ?? "2023-05-01.17.0", - timeOut: options?.timeOutInSeconds, - }, }); } @@ -3055,31 +3363,11 @@ export function _getTaskSend( taskId: string, options: GetTaskOptionalParams = { requestOptions: {} }, ): StreamableMethod { - return context.path("/jobs/{jobId}/tasks/{taskId}", jobId, taskId).get({ - ...operationOptionsToRequestParameters(options), - headers: { - ...(options?.ifMatch !== undefined - ? { "if-match": options?.ifMatch } - : {}), - ...(options?.ifNoneMatch !== undefined - ? { "if-none-match": options?.ifNoneMatch } - : {}), - ...(options?.ifModifiedSince !== undefined - ? { - "if-modified-since": !options?.ifModifiedSince - ? options?.ifModifiedSince - : options?.ifModifiedSince.toUTCString(), - } - : {}), - ...(options?.ifUnmodifiedSince !== undefined - ? { - "if-unmodified-since": !options?.ifUnmodifiedSince - ? options?.ifUnmodifiedSince - : options?.ifUnmodifiedSince.toUTCString(), - } - : {}), - }, - queryParameters: { + const path = expandUrlTemplate( + "/jobs/{jobId}/tasks/{taskId}{?api-version,timeOut,$select,$expand}", + { + jobId: jobId, + taskId: taskId, "api-version": options?.apiVersion ?? "2023-05-01.17.0", timeOut: options?.timeOutInSeconds, $select: !options?.$select @@ -3093,7 +3381,37 @@ export function _getTaskSend( return p; }), }, - }); + { + allowReserved: options?.requestOptions?.skipUrlEncoding, + }, + ); + return context + .path(path) + .get({ + ...operationOptionsToRequestParameters(options), + headers: { + ...(options?.ifMatch !== undefined + ? { "if-match": options?.ifMatch } + : {}), + ...(options?.ifNoneMatch !== undefined + ? { "if-none-match": options?.ifNoneMatch } + : {}), + ...(options?.ifModifiedSince !== undefined + ? { + "if-modified-since": !options?.ifModifiedSince + ? options?.ifModifiedSince + : options?.ifModifiedSince.toUTCString(), + } + : {}), + ...(options?.ifUnmodifiedSince !== undefined + ? { + "if-unmodified-since": !options?.ifUnmodifiedSince + ? options?.ifUnmodifiedSince + : options?.ifUnmodifiedSince.toUTCString(), + } + : {}), + }, + }); } export async function _getTaskDeserialize( @@ -3129,8 +3447,20 @@ export function _replaceTaskSend( body: BatchTask, options: ReplaceTaskOptionalParams = { requestOptions: {} }, ): StreamableMethod { + const path = expandUrlTemplate( + "/jobs/{jobId}/tasks/{taskId}{?api-version,timeOut}", + { + jobId: jobId, + taskId: taskId, + "api-version": options?.apiVersion ?? "2023-05-01.17.0", + timeOut: options?.timeOutInSeconds, + }, + { + allowReserved: options?.requestOptions?.skipUrlEncoding, + }, + ); return context - .path("/jobs/{jobId}/tasks/{taskId}", jobId, taskId) + .path(path) .put({ ...operationOptionsToRequestParameters(options), contentType: @@ -3158,10 +3488,6 @@ export function _replaceTaskSend( } : {}), }, - queryParameters: { - "api-version": options?.apiVersion ?? "2023-05-01.17.0", - timeOut: options?.timeOutInSeconds, - }, body: batchTaskSerializer(body), }); } @@ -3195,20 +3521,26 @@ export function _listSubTasksSend( taskId: string, options: ListSubTasksOptionalParams = { requestOptions: {} }, ): StreamableMethod { + const path = expandUrlTemplate( + "/jobs/{jobId}/tasks/{taskId}/subtasksinfo{?api-version,timeOut,$select}", + { + jobId: jobId, + taskId: taskId, + "api-version": options?.apiVersion ?? "2023-05-01.17.0", + timeOut: options?.timeOutInSeconds, + $select: !options?.$select + ? options?.$select + : options?.$select.map((p: any) => { + return p; + }), + }, + { + allowReserved: options?.requestOptions?.skipUrlEncoding, + }, + ); return context - .path("/jobs/{jobId}/tasks/{taskId}/subtasksinfo", jobId, taskId) - .get({ - ...operationOptionsToRequestParameters(options), - queryParameters: { - "api-version": options?.apiVersion ?? "2023-05-01.17.0", - timeOut: options?.timeOutInSeconds, - $select: !options?.$select - ? options?.$select - : options?.$select.map((p: any) => { - return p; - }), - }, - }); + .path(path) + .get({ ...operationOptionsToRequestParameters(options) }); } export async function _listSubTasksDeserialize( @@ -3239,8 +3571,20 @@ export function _terminateTaskSend( taskId: string, options: TerminateTaskOptionalParams = { requestOptions: {} }, ): StreamableMethod { + const path = expandUrlTemplate( + "/jobs/{jobId}/tasks/{taskId}/terminate{?api-version,timeOut}", + { + jobId: jobId, + taskId: taskId, + "api-version": options?.apiVersion ?? "2023-05-01.17.0", + timeOut: options?.timeOutInSeconds, + }, + { + allowReserved: options?.requestOptions?.skipUrlEncoding, + }, + ); return context - .path("/jobs/{jobId}/tasks/{taskId}/terminate", jobId, taskId) + .path(path) .post({ ...operationOptionsToRequestParameters(options), headers: { @@ -3265,10 +3609,6 @@ export function _terminateTaskSend( } : {}), }, - queryParameters: { - "api-version": options?.apiVersion ?? "2023-05-01.17.0", - timeOut: options?.timeOutInSeconds, - }, }); } @@ -3304,8 +3644,20 @@ export function _reactivateTaskSend( taskId: string, options: ReactivateTaskOptionalParams = { requestOptions: {} }, ): StreamableMethod { + const path = expandUrlTemplate( + "/jobs/{jobId}/tasks/{taskId}/reactivate{?api-version,timeOut}", + { + jobId: jobId, + taskId: taskId, + "api-version": options?.apiVersion ?? "2023-05-01.17.0", + timeOut: options?.timeOutInSeconds, + }, + { + allowReserved: options?.requestOptions?.skipUrlEncoding, + }, + ); return context - .path("/jobs/{jobId}/tasks/{taskId}/reactivate", jobId, taskId) + .path(path) .post({ ...operationOptionsToRequestParameters(options), headers: { @@ -3330,10 +3682,6 @@ export function _reactivateTaskSend( } : {}), }, - queryParameters: { - "api-version": options?.apiVersion ?? "2023-05-01.17.0", - timeOut: options?.timeOutInSeconds, - }, }); } @@ -3374,21 +3722,23 @@ export function _deleteTaskFileSend( filePath: string, options: DeleteTaskFileOptionalParams = { requestOptions: {} }, ): StreamableMethod { + const path = expandUrlTemplate( + "/jobs/{jobId}/tasks/{taskId}/files/{filePath}{?api-version,timeOut,recursive}", + { + jobId: jobId, + taskId: taskId, + filePath: filePath, + "api-version": options?.apiVersion ?? "2023-05-01.17.0", + timeOut: options?.timeOutInSeconds, + recursive: options?.recursive, + }, + { + allowReserved: options?.requestOptions?.skipUrlEncoding, + }, + ); return context - .path( - "/jobs/{jobId}/tasks/{taskId}/files/{filePath}", - jobId, - taskId, - filePath, - ) - .delete({ - ...operationOptionsToRequestParameters(options), - queryParameters: { - "api-version": options?.apiVersion ?? "2023-05-01.17.0", - timeOut: options?.timeOutInSeconds, - recursive: options?.recursive, - }, - }); + .path(path) + .delete({ ...operationOptionsToRequestParameters(options) }); } export async function _deleteTaskFileDeserialize( @@ -3427,13 +3777,21 @@ export function _getTaskFileSend( filePath: string, options: GetTaskFileOptionalParams = { requestOptions: {} }, ): StreamableMethod { + const path = expandUrlTemplate( + "/jobs/{jobId}/tasks/{taskId}/files/{filePath}{?api-version,timeOut}", + { + jobId: jobId, + taskId: taskId, + filePath: filePath, + "api-version": options?.apiVersion ?? "2023-05-01.17.0", + timeOut: options?.timeOutInSeconds, + }, + { + allowReserved: options?.requestOptions?.skipUrlEncoding, + }, + ); return context - .path( - "/jobs/{jobId}/tasks/{taskId}/files/{filePath}", - jobId, - taskId, - filePath, - ) + .path(path) .get({ ...operationOptionsToRequestParameters(options), headers: { @@ -3455,10 +3813,6 @@ export function _getTaskFileSend( ? { "ocp-range": options?.ocpRange } : {}), }, - queryParameters: { - "api-version": options?.apiVersion ?? "2023-05-01.17.0", - timeOut: options?.timeOutInSeconds, - }, }); } @@ -3498,13 +3852,21 @@ export function _getTaskFilePropertiesSend( filePath: string, options: GetTaskFilePropertiesOptionalParams = { requestOptions: {} }, ): StreamableMethod { + const path = expandUrlTemplate( + "/jobs/{jobId}/tasks/{taskId}/files/{filePath}{?api-version,timeOut}", + { + jobId: jobId, + taskId: taskId, + filePath: filePath, + "api-version": options?.apiVersion ?? "2023-05-01.17.0", + timeOut: options?.timeOutInSeconds, + }, + { + allowReserved: options?.requestOptions?.skipUrlEncoding, + }, + ); return context - .path( - "/jobs/{jobId}/tasks/{taskId}/files/{filePath}", - jobId, - taskId, - filePath, - ) + .path(path) .head({ ...operationOptionsToRequestParameters(options), headers: { @@ -3523,10 +3885,6 @@ export function _getTaskFilePropertiesSend( } : {}), }, - queryParameters: { - "api-version": options?.apiVersion ?? "2023-05-01.17.0", - timeOut: options?.timeOutInSeconds, - }, }); } @@ -3565,18 +3923,24 @@ export function _listTaskFilesSend( taskId: string, options: ListTaskFilesOptionalParams = { requestOptions: {} }, ): StreamableMethod { + const path = expandUrlTemplate( + "/jobs/{jobId}/tasks/{taskId}/files{?api-version,maxresults,timeOut,$filter,recursive}", + { + jobId: jobId, + taskId: taskId, + "api-version": options?.apiVersion ?? "2023-05-01.17.0", + maxresults: options?.maxresults, + timeOut: options?.timeOutInSeconds, + $filter: options?.$filter, + recursive: options?.recursive, + }, + { + allowReserved: options?.requestOptions?.skipUrlEncoding, + }, + ); return context - .path("/jobs/{jobId}/tasks/{taskId}/files", jobId, taskId) - .get({ - ...operationOptionsToRequestParameters(options), - queryParameters: { - "api-version": options?.apiVersion ?? "2023-05-01.17.0", - maxresults: options?.maxresults, - timeOut: options?.timeOutInSeconds, - $filter: options?.$filter, - recursive: options?.recursive, - }, - }); + .path(path) + .get({ ...operationOptionsToRequestParameters(options) }); } export async function _listTaskFilesDeserialize( @@ -3613,17 +3977,25 @@ export function _createNodeUserSend( body: BatchNodeUserCreateOptions, options: CreateNodeUserOptionalParams = { requestOptions: {} }, ): StreamableMethod { + const path = expandUrlTemplate( + "/pools/{poolId}/nodes/{nodeId}/users{?api-version,timeOut}", + { + poolId: poolId, + nodeId: nodeId, + "api-version": options?.apiVersion ?? "2023-05-01.17.0", + timeOut: options?.timeOutInSeconds, + }, + { + allowReserved: options?.requestOptions?.skipUrlEncoding, + }, + ); return context - .path("/pools/{poolId}/nodes/{nodeId}/users", poolId, nodeId) + .path(path) .post({ ...operationOptionsToRequestParameters(options), contentType: (options.contentType as any) ?? "application/json; odata=minimalmetadata", - queryParameters: { - "api-version": options?.apiVersion ?? "2023-05-01.17.0", - timeOut: options?.timeOutInSeconds, - }, body: batchNodeUserCreateOptionsSerializer(body), }); } @@ -3667,20 +4039,22 @@ export function _deleteNodeUserSend( userName: string, options: DeleteNodeUserOptionalParams = { requestOptions: {} }, ): StreamableMethod { + const path = expandUrlTemplate( + "/pools/{poolId}/nodes/{nodeId}/users/{userName}{?api-version,timeOut}", + { + poolId: poolId, + nodeId: nodeId, + userName: userName, + "api-version": options?.apiVersion ?? "2023-05-01.17.0", + timeOut: options?.timeOutInSeconds, + }, + { + allowReserved: options?.requestOptions?.skipUrlEncoding, + }, + ); return context - .path( - "/pools/{poolId}/nodes/{nodeId}/users/{userName}", - poolId, - nodeId, - userName, - ) - .delete({ - ...operationOptionsToRequestParameters(options), - queryParameters: { - "api-version": options?.apiVersion ?? "2023-05-01.17.0", - timeOut: options?.timeOutInSeconds, - }, - }); + .path(path) + .delete({ ...operationOptionsToRequestParameters(options) }); } export async function _deleteNodeUserDeserialize( @@ -3723,22 +4097,26 @@ export function _replaceNodeUserSend( body: BatchNodeUserUpdateOptions, options: ReplaceNodeUserOptionalParams = { requestOptions: {} }, ): StreamableMethod { + const path = expandUrlTemplate( + "/pools/{poolId}/nodes/{nodeId}/users/{userName}{?api-version,timeOut}", + { + poolId: poolId, + nodeId: nodeId, + userName: userName, + "api-version": options?.apiVersion ?? "2023-05-01.17.0", + timeOut: options?.timeOutInSeconds, + }, + { + allowReserved: options?.requestOptions?.skipUrlEncoding, + }, + ); return context - .path( - "/pools/{poolId}/nodes/{nodeId}/users/{userName}", - poolId, - nodeId, - userName, - ) + .path(path) .put({ ...operationOptionsToRequestParameters(options), contentType: (options.contentType as any) ?? "application/json; odata=minimalmetadata", - queryParameters: { - "api-version": options?.apiVersion ?? "2023-05-01.17.0", - timeOut: options?.timeOutInSeconds, - }, body: batchNodeUserUpdateOptionsSerializer(body), }); } @@ -3785,9 +4163,11 @@ export function _getNodeSend( nodeId: string, options: GetNodeOptionalParams = { requestOptions: {} }, ): StreamableMethod { - return context.path("/pools/{poolId}/nodes/{nodeId}", poolId, nodeId).get({ - ...operationOptionsToRequestParameters(options), - queryParameters: { + const path = expandUrlTemplate( + "/pools/{poolId}/nodes/{nodeId}{?api-version,timeOut,$select}", + { + poolId: poolId, + nodeId: nodeId, "api-version": options?.apiVersion ?? "2023-05-01.17.0", timeOut: options?.timeOutInSeconds, $select: !options?.$select @@ -3796,7 +4176,13 @@ export function _getNodeSend( return p; }), }, - }); + { + allowReserved: options?.requestOptions?.skipUrlEncoding, + }, + ); + return context + .path(path) + .get({ ...operationOptionsToRequestParameters(options) }); } export async function _getNodeDeserialize( @@ -3827,17 +4213,25 @@ export function _rebootNodeSend( nodeId: string, options: RebootNodeOptionalParams = { requestOptions: {} }, ): StreamableMethod { + const path = expandUrlTemplate( + "/pools/{poolId}/nodes/{nodeId}/reboot{?api-version,timeOut}", + { + poolId: poolId, + nodeId: nodeId, + "api-version": options?.apiVersion ?? "2023-05-01.17.0", + timeOut: options?.timeOutInSeconds, + }, + { + allowReserved: options?.requestOptions?.skipUrlEncoding, + }, + ); return context - .path("/pools/{poolId}/nodes/{nodeId}/reboot", poolId, nodeId) + .path(path) .post({ ...operationOptionsToRequestParameters(options), contentType: (options.contentType as any) ?? "application/json; odata=minimalmetadata", - queryParameters: { - "api-version": options?.apiVersion ?? "2023-05-01.17.0", - timeOut: options?.timeOutInSeconds, - }, body: !options["body"] ? options["body"] : nodeRebootOptionsSerializer(options["body"]), @@ -3872,17 +4266,25 @@ export function _reimageNodeSend( nodeId: string, options: ReimageNodeOptionalParams = { requestOptions: {} }, ): StreamableMethod { + const path = expandUrlTemplate( + "/pools/{poolId}/nodes/{nodeId}/reimage{?api-version,timeOut}", + { + poolId: poolId, + nodeId: nodeId, + "api-version": options?.apiVersion ?? "2023-05-01.17.0", + timeOut: options?.timeOutInSeconds, + }, + { + allowReserved: options?.requestOptions?.skipUrlEncoding, + }, + ); return context - .path("/pools/{poolId}/nodes/{nodeId}/reimage", poolId, nodeId) + .path(path) .post({ ...operationOptionsToRequestParameters(options), contentType: (options.contentType as any) ?? "application/json; odata=minimalmetadata", - queryParameters: { - "api-version": options?.apiVersion ?? "2023-05-01.17.0", - timeOut: options?.timeOutInSeconds, - }, body: !options["body"] ? options["body"] : nodeReimageOptionsSerializer(options["body"]), @@ -3921,17 +4323,25 @@ export function _disableNodeSchedulingSend( nodeId: string, options: DisableNodeSchedulingOptionalParams = { requestOptions: {} }, ): StreamableMethod { + const path = expandUrlTemplate( + "/pools/{poolId}/nodes/{nodeId}/disablescheduling{?api-version,timeOut}", + { + poolId: poolId, + nodeId: nodeId, + "api-version": options?.apiVersion ?? "2023-05-01.17.0", + timeOut: options?.timeOutInSeconds, + }, + { + allowReserved: options?.requestOptions?.skipUrlEncoding, + }, + ); return context - .path("/pools/{poolId}/nodes/{nodeId}/disablescheduling", poolId, nodeId) + .path(path) .post({ ...operationOptionsToRequestParameters(options), contentType: (options.contentType as any) ?? "application/json; odata=minimalmetadata", - queryParameters: { - "api-version": options?.apiVersion ?? "2023-05-01.17.0", - timeOut: options?.timeOutInSeconds, - }, body: !options["body"] ? options["body"] : nodeDisableSchedulingOptionsSerializer(options["body"]), @@ -3974,15 +4384,21 @@ export function _enableNodeSchedulingSend( nodeId: string, options: EnableNodeSchedulingOptionalParams = { requestOptions: {} }, ): StreamableMethod { + const path = expandUrlTemplate( + "/pools/{poolId}/nodes/{nodeId}/enablescheduling{?api-version,timeOut}", + { + poolId: poolId, + nodeId: nodeId, + "api-version": options?.apiVersion ?? "2023-05-01.17.0", + timeOut: options?.timeOutInSeconds, + }, + { + allowReserved: options?.requestOptions?.skipUrlEncoding, + }, + ); return context - .path("/pools/{poolId}/nodes/{nodeId}/enablescheduling", poolId, nodeId) - .post({ - ...operationOptionsToRequestParameters(options), - queryParameters: { - "api-version": options?.apiVersion ?? "2023-05-01.17.0", - timeOut: options?.timeOutInSeconds, - }, - }); + .path(path) + .post({ ...operationOptionsToRequestParameters(options) }); } export async function _enableNodeSchedulingDeserialize( @@ -4021,15 +4437,21 @@ export function _getNodeRemoteLoginSettingsSend( nodeId: string, options: GetNodeRemoteLoginSettingsOptionalParams = { requestOptions: {} }, ): StreamableMethod { + const path = expandUrlTemplate( + "/pools/{poolId}/nodes/{nodeId}/remoteloginsettings{?api-version,timeOut}", + { + poolId: poolId, + nodeId: nodeId, + "api-version": options?.apiVersion ?? "2023-05-01.17.0", + timeOut: options?.timeOutInSeconds, + }, + { + allowReserved: options?.requestOptions?.skipUrlEncoding, + }, + ); return context - .path("/pools/{poolId}/nodes/{nodeId}/remoteloginsettings", poolId, nodeId) - .get({ - ...operationOptionsToRequestParameters(options), - queryParameters: { - "api-version": options?.apiVersion ?? "2023-05-01.17.0", - timeOut: options?.timeOutInSeconds, - }, - }); + .path(path) + .get({ ...operationOptionsToRequestParameters(options) }); } export async function _getNodeRemoteLoginSettingsDeserialize( @@ -4071,15 +4493,21 @@ export function _getNodeRemoteDesktopFileSend( nodeId: string, options: GetNodeRemoteDesktopFileOptionalParams = { requestOptions: {} }, ): StreamableMethod { + const path = expandUrlTemplate( + "/pools/{poolId}/nodes/{nodeId}/rdp{?api-version,timeOut}", + { + poolId: poolId, + nodeId: nodeId, + "api-version": options?.apiVersion ?? "2023-05-01.17.0", + timeOut: options?.timeOutInSeconds, + }, + { + allowReserved: options?.requestOptions?.skipUrlEncoding, + }, + ); return context - .path("/pools/{poolId}/nodes/{nodeId}/rdp", poolId, nodeId) - .get({ - ...operationOptionsToRequestParameters(options), - queryParameters: { - "api-version": options?.apiVersion ?? "2023-05-01.17.0", - timeOut: options?.timeOutInSeconds, - }, - }); + .path(path) + .get({ ...operationOptionsToRequestParameters(options) }); } export async function _getNodeRemoteDesktopFileDeserialize( @@ -4123,21 +4551,25 @@ export function _uploadNodeLogsSend( body: UploadBatchServiceLogsOptions, options: UploadNodeLogsOptionalParams = { requestOptions: {} }, ): StreamableMethod { + const path = expandUrlTemplate( + "/pools/{poolId}/nodes/{nodeId}/uploadbatchservicelogs{?api-version,timeOut}", + { + poolId: poolId, + nodeId: nodeId, + "api-version": options?.apiVersion ?? "2023-05-01.17.0", + timeOut: options?.timeOutInSeconds, + }, + { + allowReserved: options?.requestOptions?.skipUrlEncoding, + }, + ); return context - .path( - "/pools/{poolId}/nodes/{nodeId}/uploadbatchservicelogs", - poolId, - nodeId, - ) + .path(path) .post({ ...operationOptionsToRequestParameters(options), contentType: (options.contentType as any) ?? "application/json; odata=minimalmetadata", - queryParameters: { - "api-version": options?.apiVersion ?? "2023-05-01.17.0", - timeOut: options?.timeOutInSeconds, - }, body: uploadBatchServiceLogsOptionsSerializer(body), }); } @@ -4181,9 +4613,10 @@ export function _listNodesSend( poolId: string, options: ListNodesOptionalParams = { requestOptions: {} }, ): StreamableMethod { - return context.path("/pools/{poolId}/nodes", poolId).get({ - ...operationOptionsToRequestParameters(options), - queryParameters: { + const path = expandUrlTemplate( + "/pools/{poolId}/nodes{?api-version,maxresults,timeOut,$filter,$select}", + { + poolId: poolId, "api-version": options?.apiVersion ?? "2023-05-01.17.0", maxresults: options?.maxresults, timeOut: options?.timeOutInSeconds, @@ -4194,7 +4627,13 @@ export function _listNodesSend( return p; }), }, - }); + { + allowReserved: options?.requestOptions?.skipUrlEncoding, + }, + ); + return context + .path(path) + .get({ ...operationOptionsToRequestParameters(options) }); } export async function _listNodesDeserialize( @@ -4230,25 +4669,27 @@ export function _getNodeExtensionSend( extensionName: string, options: GetNodeExtensionOptionalParams = { requestOptions: {} }, ): StreamableMethod { + const path = expandUrlTemplate( + "/pools/{poolId}/nodes/{nodeId}/extensions/{extensionName}{?api-version,timeOut,$select}", + { + poolId: poolId, + nodeId: nodeId, + extensionName: extensionName, + "api-version": options?.apiVersion ?? "2023-05-01.17.0", + timeOut: options?.timeOutInSeconds, + $select: !options?.$select + ? options?.$select + : options?.$select.map((p: any) => { + return p; + }), + }, + { + allowReserved: options?.requestOptions?.skipUrlEncoding, + }, + ); return context - .path( - "/pools/{poolId}/nodes/{nodeId}/extensions/{extensionName}", - poolId, - nodeId, - extensionName, - ) - .get({ - ...operationOptionsToRequestParameters(options), - queryParameters: { - "api-version": options?.apiVersion ?? "2023-05-01.17.0", - timeOut: options?.timeOutInSeconds, - $select: !options?.$select - ? options?.$select - : options?.$select.map((p: any) => { - return p; - }), - }, - }); + .path(path) + .get({ ...operationOptionsToRequestParameters(options) }); } export async function _getNodeExtensionDeserialize( @@ -4286,20 +4727,26 @@ export function _listNodeExtensionsSend( nodeId: string, options: ListNodeExtensionsOptionalParams = { requestOptions: {} }, ): StreamableMethod { + const path = expandUrlTemplate( + "/pools/{poolId}/nodes/{nodeId}/extensions{?maxresults,timeOut,$select}", + { + poolId: poolId, + nodeId: nodeId, + maxresults: options?.maxresults, + timeOut: options?.timeOutInSeconds, + $select: !options?.$select + ? options?.$select + : options?.$select.map((p: any) => { + return p; + }), + }, + { + allowReserved: options?.requestOptions?.skipUrlEncoding, + }, + ); return context - .path("/pools/{poolId}/nodes/{nodeId}/extensions", poolId, nodeId) - .get({ - ...operationOptionsToRequestParameters(options), - queryParameters: { - maxresults: options?.maxresults, - timeOut: options?.timeOutInSeconds, - $select: !options?.$select - ? options?.$select - : options?.$select.map((p: any) => { - return p; - }), - }, - }); + .path(path) + .get({ ...operationOptionsToRequestParameters(options) }); } export async function _listNodeExtensionsDeserialize( @@ -4336,21 +4783,23 @@ export function _deleteNodeFileSend( filePath: string, options: DeleteNodeFileOptionalParams = { requestOptions: {} }, ): StreamableMethod { + const path = expandUrlTemplate( + "/pools/{poolId}/nodes/{nodeId}/files/{filePath}{?api-version,timeOut,recursive}", + { + poolId: poolId, + nodeId: nodeId, + filePath: filePath, + "api-version": options?.apiVersion ?? "2023-05-01.17.0", + timeOut: options?.timeOutInSeconds, + recursive: options?.recursive, + }, + { + allowReserved: options?.requestOptions?.skipUrlEncoding, + }, + ); return context - .path( - "/pools/{poolId}/nodes/{nodeId}/files/{filePath}", - poolId, - nodeId, - filePath, - ) - .delete({ - ...operationOptionsToRequestParameters(options), - queryParameters: { - "api-version": options?.apiVersion ?? "2023-05-01.17.0", - timeOut: options?.timeOutInSeconds, - recursive: options?.recursive, - }, - }); + .path(path) + .delete({ ...operationOptionsToRequestParameters(options) }); } export async function _deleteNodeFileDeserialize( @@ -4389,13 +4838,21 @@ export function _getNodeFileSend( filePath: string, options: GetNodeFileOptionalParams = { requestOptions: {} }, ): StreamableMethod { + const path = expandUrlTemplate( + "/pools/{poolId}/nodes/{nodeId}/files/{filePath}{?api-version,timeOut}", + { + poolId: poolId, + nodeId: nodeId, + filePath: filePath, + "api-version": options?.apiVersion ?? "2023-05-01.17.0", + timeOut: options?.timeOutInSeconds, + }, + { + allowReserved: options?.requestOptions?.skipUrlEncoding, + }, + ); return context - .path( - "/pools/{poolId}/nodes/{nodeId}/files/{filePath}", - poolId, - nodeId, - filePath, - ) + .path(path) .get({ ...operationOptionsToRequestParameters(options), headers: { @@ -4417,10 +4874,6 @@ export function _getNodeFileSend( ? { "ocp-range": options?.ocpRange } : {}), }, - queryParameters: { - "api-version": options?.apiVersion ?? "2023-05-01.17.0", - timeOut: options?.timeOutInSeconds, - }, }); } @@ -4462,13 +4915,21 @@ export function _getNodeFilePropertiesSend( filePath: string, options: GetNodeFilePropertiesOptionalParams = { requestOptions: {} }, ): StreamableMethod { + const path = expandUrlTemplate( + "/pools/{poolId}/nodes/{nodeId}/files/{filePath}{?api-version,timeOut}", + { + poolId: poolId, + nodeId: nodeId, + filePath: filePath, + "api-version": options?.apiVersion ?? "2023-05-01.17.0", + timeOut: options?.timeOutInSeconds, + }, + { + allowReserved: options?.requestOptions?.skipUrlEncoding, + }, + ); return context - .path( - "/pools/{poolId}/nodes/{nodeId}/files/{filePath}", - poolId, - nodeId, - filePath, - ) + .path(path) .head({ ...operationOptionsToRequestParameters(options), headers: { @@ -4487,10 +4948,6 @@ export function _getNodeFilePropertiesSend( } : {}), }, - queryParameters: { - "api-version": options?.apiVersion ?? "2023-05-01.17.0", - timeOut: options?.timeOutInSeconds, - }, }); } @@ -4529,18 +4986,24 @@ export function _listNodeFilesSend( nodeId: string, options: ListNodeFilesOptionalParams = { requestOptions: {} }, ): StreamableMethod { + const path = expandUrlTemplate( + "/pools/{poolId}/nodes/{nodeId}/files{?api-version,maxresults,timeOut,$filter,recursive}", + { + poolId: poolId, + nodeId: nodeId, + "api-version": options?.apiVersion ?? "2023-05-01.17.0", + maxresults: options?.maxresults, + timeOut: options?.timeOutInSeconds, + $filter: options?.$filter, + recursive: options?.recursive, + }, + { + allowReserved: options?.requestOptions?.skipUrlEncoding, + }, + ); return context - .path("/pools/{poolId}/nodes/{nodeId}/files", poolId, nodeId) - .get({ - ...operationOptionsToRequestParameters(options), - queryParameters: { - "api-version": options?.apiVersion ?? "2023-05-01.17.0", - maxresults: options?.maxresults, - timeOut: options?.timeOutInSeconds, - $filter: options?.$filter, - recursive: options?.recursive, - }, - }); + .path(path) + .get({ ...operationOptionsToRequestParameters(options) }); } export async function _listNodeFilesDeserialize( diff --git a/packages/typespec-test/test/batch_modular/generated/typespec-ts/src/static-helpers/urlTemplate.ts b/packages/typespec-test/test/batch_modular/generated/typespec-ts/src/static-helpers/urlTemplate.ts new file mode 100644 index 0000000000..3d56a97a31 --- /dev/null +++ b/packages/typespec-test/test/batch_modular/generated/typespec-ts/src/static-helpers/urlTemplate.ts @@ -0,0 +1,199 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +//--------------------- +// interfaces +//--------------------- +interface ValueOptions { + isFirst: boolean; // is first value in the expression + op?: string; // operator + varValue?: any; // variable value + varName?: string; // variable name + modifier?: string; // modifier e.g * + reserved: boolean; // if true we'll keep reserved words with not encoding +} + +export interface UrlTemplateOptions { + // if set to true, reserved characters will not be encoded + allowReserved?: boolean; +} + +// --------------------- +// helpers +// --------------------- +function encodeValue(val: string, reserved: boolean, op?: string) { + return reserved === true || op === "+" || op === "#" + ? encodeWithReserved(val) + : encodeRFC3986URIComponent(val); +} + +function encodeWithReserved(str: string) { + return str + .split(/(%[0-9A-Fa-f]{2})/g) + .map((part) => (!/%[0-9A-Fa-f]/.test(part) ? encodeURI(part) : part)) + .join(""); +} + +function encodeRFC3986URIComponent(str: string) { + return encodeURIComponent(str).replace( + /[!'()*]/g, + (c) => `%${c.charCodeAt(0).toString(16).toUpperCase()}`, + ); +} + +function isDefined(val: any) { + return val !== undefined && val !== null; +} + +function getNamedAndIfEmpty(op?: string): [boolean, string] { + return [ + !!op && [";", "?", "&"].includes(op), + !!op && ["?", "&"].includes(op) ? "=" : "", + ]; +} + +function getFirstOrSep(op?: string, isFirst = false) { + if (isFirst) { + return !op || op === "+" ? "" : op; + } else if (!op || op === "+" || op === "#") { + return ","; + } else if (op === "?") { + return "&"; + } else { + return op; + } +} + +function getExpandedValue(option: ValueOptions) { + let isFirst = option.isFirst; + const { op, varName, varValue: value, reserved } = option; + const vals: string[] = []; + const [named, ifEmpty] = getNamedAndIfEmpty(op); + + if (Array.isArray(value)) { + for (const val of value.filter(isDefined)) { + // prepare the following parts: separator, varName, value + vals.push(`${getFirstOrSep(op, isFirst)}`); + if (named && varName) { + vals.push(`${encodeURIComponent(varName)}`); + val === "" ? vals.push(ifEmpty) : vals.push("="); + } + vals.push(encodeValue(val, reserved, op)); + isFirst = false; + } + } else if (typeof value === "object") { + for (const key of Object.keys(value)) { + const val = value[key]; + if (!isDefined(val)) { + continue; + } + // prepare the following parts: separator, key, value + vals.push(`${getFirstOrSep(op, isFirst)}`); + if (key) { + vals.push(`${encodeURIComponent(key)}`); + named && val === "" ? vals.push(ifEmpty) : vals.push("="); + } + vals.push(encodeValue(val, reserved, op)); + isFirst = false; + } + } + return vals.join(""); +} + +function getNonExpandedValue(option: ValueOptions) { + const { op, varName, varValue: value, isFirst, reserved } = option; + const vals: string[] = []; + const first = getFirstOrSep(op, isFirst); + const [named, ifEmpty] = getNamedAndIfEmpty(op); + if (named && varName) { + vals.push(encodeValue(varName, reserved, op)); + if (value === "") { + if (!ifEmpty) { + vals.push(ifEmpty); + } + return !vals.join("") ? undefined : `${first}${vals.join("")}`; + } + vals.push("="); + } + + const items = []; + if (Array.isArray(value)) { + for (const val of value.filter(isDefined)) { + items.push(encodeValue(val, reserved, op)); + } + } else if (typeof value === "object") { + for (const key of Object.keys(value)) { + if (!isDefined(value[key])) { + continue; + } + items.push(encodeRFC3986URIComponent(key)); + items.push(encodeValue(value[key], reserved, op)); + } + } + vals.push(items.join(",")); + return !vals.join(",") ? undefined : `${first}${vals.join("")}`; +} + +function getVarValue(option: ValueOptions): string | undefined { + const { op, varName, modifier, isFirst, reserved, varValue: value } = option; + + if (!isDefined(value)) { + return undefined; + } else if (["string", "number", "boolean"].includes(typeof value)) { + let val = value.toString(); + const [named, ifEmpty] = getNamedAndIfEmpty(op); + const vals: string[] = [getFirstOrSep(op, isFirst)]; + if (named && varName) { + vals.push(encodeRFC3986URIComponent(varName)); + val === "" ? vals.push(ifEmpty) : vals.push("="); + } + if (modifier && modifier !== "*") { + val = val.substring(0, parseInt(modifier, 10)); + } + vals.push(encodeValue(val, reserved, op)); + return vals.join(""); + } else if (modifier === "*") { + return getExpandedValue(option); + } else { + return getNonExpandedValue(option); + } +} + +// --------------------------------------------------------------------------------------------------- +// This is an implementation of RFC 6570 URI Template: https://datatracker.ietf.org/doc/html/rfc6570. +// --------------------------------------------------------------------------------------------------- +export function expandUrlTemplate( + template: string, + context: Record, + option?: UrlTemplateOptions, +): string { + return template.replace(/\{([^\{\}]+)\}|([^\{\}]+)/g, (_, expr, text) => { + if (!expr) { + return encodeWithReserved(text); + } + let op; + if (["+", "#", ".", "/", ";", "?", "&"].includes(expr[0])) { + (op = expr[0]), (expr = expr.slice(1)); + } + const varList = expr.split(/,/g); + const result = []; + for (const varSpec of varList) { + const varMatch = /([^:\*]*)(?::(\d+)|(\*))?/.exec(varSpec); + if (!varMatch || !varMatch[1]) { + continue; + } + const varValue = getVarValue({ + isFirst: result.length === 0, + op, + varValue: context[varMatch[1]], + varName: varMatch[1], + modifier: varMatch[2] || varMatch[3], + reserved: option?.allowReserved ?? false, + }); + if (varValue) { + result.push(varValue); + } + } + return result.join(""); + }); +} diff --git a/packages/typespec-test/test/contentsafety_modular/generated/typespec-ts/src/api/operations.ts b/packages/typespec-test/test/contentsafety_modular/generated/typespec-ts/src/api/operations.ts index 9620a7e5c4..e6fbfa41d7 100644 --- a/packages/typespec-test/test/contentsafety_modular/generated/typespec-ts/src/api/operations.ts +++ b/packages/typespec-test/test/contentsafety_modular/generated/typespec-ts/src/api/operations.ts @@ -43,6 +43,7 @@ import { PagedAsyncIterableIterator, buildPagedAsyncIterator, } from "../static-helpers/pagingHelpers.js"; +import { expandUrlTemplate } from "../static-helpers/urlTemplate.js"; import { StreamableMethod, PathUncheckedResponse, @@ -123,8 +124,17 @@ export function _getTextBlocklistSend( blocklistName: string, options: GetTextBlocklistOptionalParams = { requestOptions: {} }, ): StreamableMethod { + const path = expandUrlTemplate( + "/text/blocklists/{blocklistName}{?api-version}", + { + blocklistName: blocklistName, + }, + { + allowReserved: options?.requestOptions?.skipUrlEncoding, + }, + ); return context - .path("/text/blocklists/{blocklistName}", blocklistName) + .path(path) .get({ ...operationOptionsToRequestParameters(options) }); } @@ -155,8 +165,17 @@ export function _createOrUpdateTextBlocklistSend( resource: TextBlocklist, options: CreateOrUpdateTextBlocklistOptionalParams = { requestOptions: {} }, ): StreamableMethod { + const path = expandUrlTemplate( + "/text/blocklists/{blocklistName}{?api-version}", + { + blocklistName: blocklistName, + }, + { + allowReserved: options?.requestOptions?.skipUrlEncoding, + }, + ); return context - .path("/text/blocklists/{blocklistName}", blocklistName) + .path(path) .patch({ ...operationOptionsToRequestParameters(options), contentType: @@ -197,8 +216,17 @@ export function _deleteTextBlocklistSend( blocklistName: string, options: DeleteTextBlocklistOptionalParams = { requestOptions: {} }, ): StreamableMethod { + const path = expandUrlTemplate( + "/text/blocklists/{blocklistName}{?api-version}", + { + blocklistName: blocklistName, + }, + { + allowReserved: options?.requestOptions?.skipUrlEncoding, + }, + ); return context - .path("/text/blocklists/{blocklistName}", blocklistName) + .path(path) .delete({ ...operationOptionsToRequestParameters(options) }); } @@ -267,11 +295,17 @@ export function _addOrUpdateBlockItemsSend( body: AddOrUpdateBlockItemsOptions, options: AddOrUpdateBlockItemsOptionalParams = { requestOptions: {} }, ): StreamableMethod { + const path = expandUrlTemplate( + "/text/blocklists/{blocklistName}:addOrUpdateBlockItems{?api-version}", + { + blocklistName: blocklistName, + }, + { + allowReserved: options?.requestOptions?.skipUrlEncoding, + }, + ); return context - .path( - "/text/blocklists/{blocklistName}:addOrUpdateBlockItems", - blocklistName, - ) + .path(path) .post({ ...operationOptionsToRequestParameters(options), body: addOrUpdateBlockItemsOptionsSerializer(body), @@ -311,8 +345,17 @@ export function _removeBlockItemsSend( body: RemoveBlockItemsOptions, options: RemoveBlockItemsOptionalParams = { requestOptions: {} }, ): StreamableMethod { + const path = expandUrlTemplate( + "/text/blocklists/{blocklistName}:removeBlockItems{?api-version}", + { + blocklistName: blocklistName, + }, + { + allowReserved: options?.requestOptions?.skipUrlEncoding, + }, + ); return context - .path("/text/blocklists/{blocklistName}:removeBlockItems", blocklistName) + .path(path) .post({ ...operationOptionsToRequestParameters(options), body: removeBlockItemsOptionsSerializer(body), @@ -352,12 +395,18 @@ export function _getTextBlocklistItemSend( blockItemId: string, options: GetTextBlocklistItemOptionalParams = { requestOptions: {} }, ): StreamableMethod { + const path = expandUrlTemplate( + "/text/blocklists/{blocklistName}/blockItems/{blockItemId}{?api-version}", + { + blocklistName: blocklistName, + blockItemId: blockItemId, + }, + { + allowReserved: options?.requestOptions?.skipUrlEncoding, + }, + ); return context - .path( - "/text/blocklists/{blocklistName}/blockItems/{blockItemId}", - blocklistName, - blockItemId, - ) + .path(path) .get({ ...operationOptionsToRequestParameters(options) }); } @@ -393,16 +442,21 @@ export function _listTextBlocklistItemsSend( blocklistName: string, options: ListTextBlocklistItemsOptionalParams = { requestOptions: {} }, ): StreamableMethod { + const path = expandUrlTemplate( + "/text/blocklists/{blocklistName}/blockItems{?api-version,top,skip,maxpagesize}", + { + blocklistName: blocklistName, + top: options?.top, + skip: options?.skip, + maxpagesize: options?.maxpagesize, + }, + { + allowReserved: options?.requestOptions?.skipUrlEncoding, + }, + ); return context - .path("/text/blocklists/{blocklistName}/blockItems", blocklistName) - .get({ - ...operationOptionsToRequestParameters(options), - queryParameters: { - top: options?.top, - skip: options?.skip, - maxpagesize: options?.maxpagesize, - }, - }); + .path(path) + .get({ ...operationOptionsToRequestParameters(options) }); } export async function _listTextBlocklistItemsDeserialize( diff --git a/packages/typespec-test/test/contentsafety_modular/generated/typespec-ts/src/static-helpers/urlTemplate.ts b/packages/typespec-test/test/contentsafety_modular/generated/typespec-ts/src/static-helpers/urlTemplate.ts new file mode 100644 index 0000000000..3d56a97a31 --- /dev/null +++ b/packages/typespec-test/test/contentsafety_modular/generated/typespec-ts/src/static-helpers/urlTemplate.ts @@ -0,0 +1,199 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +//--------------------- +// interfaces +//--------------------- +interface ValueOptions { + isFirst: boolean; // is first value in the expression + op?: string; // operator + varValue?: any; // variable value + varName?: string; // variable name + modifier?: string; // modifier e.g * + reserved: boolean; // if true we'll keep reserved words with not encoding +} + +export interface UrlTemplateOptions { + // if set to true, reserved characters will not be encoded + allowReserved?: boolean; +} + +// --------------------- +// helpers +// --------------------- +function encodeValue(val: string, reserved: boolean, op?: string) { + return reserved === true || op === "+" || op === "#" + ? encodeWithReserved(val) + : encodeRFC3986URIComponent(val); +} + +function encodeWithReserved(str: string) { + return str + .split(/(%[0-9A-Fa-f]{2})/g) + .map((part) => (!/%[0-9A-Fa-f]/.test(part) ? encodeURI(part) : part)) + .join(""); +} + +function encodeRFC3986URIComponent(str: string) { + return encodeURIComponent(str).replace( + /[!'()*]/g, + (c) => `%${c.charCodeAt(0).toString(16).toUpperCase()}`, + ); +} + +function isDefined(val: any) { + return val !== undefined && val !== null; +} + +function getNamedAndIfEmpty(op?: string): [boolean, string] { + return [ + !!op && [";", "?", "&"].includes(op), + !!op && ["?", "&"].includes(op) ? "=" : "", + ]; +} + +function getFirstOrSep(op?: string, isFirst = false) { + if (isFirst) { + return !op || op === "+" ? "" : op; + } else if (!op || op === "+" || op === "#") { + return ","; + } else if (op === "?") { + return "&"; + } else { + return op; + } +} + +function getExpandedValue(option: ValueOptions) { + let isFirst = option.isFirst; + const { op, varName, varValue: value, reserved } = option; + const vals: string[] = []; + const [named, ifEmpty] = getNamedAndIfEmpty(op); + + if (Array.isArray(value)) { + for (const val of value.filter(isDefined)) { + // prepare the following parts: separator, varName, value + vals.push(`${getFirstOrSep(op, isFirst)}`); + if (named && varName) { + vals.push(`${encodeURIComponent(varName)}`); + val === "" ? vals.push(ifEmpty) : vals.push("="); + } + vals.push(encodeValue(val, reserved, op)); + isFirst = false; + } + } else if (typeof value === "object") { + for (const key of Object.keys(value)) { + const val = value[key]; + if (!isDefined(val)) { + continue; + } + // prepare the following parts: separator, key, value + vals.push(`${getFirstOrSep(op, isFirst)}`); + if (key) { + vals.push(`${encodeURIComponent(key)}`); + named && val === "" ? vals.push(ifEmpty) : vals.push("="); + } + vals.push(encodeValue(val, reserved, op)); + isFirst = false; + } + } + return vals.join(""); +} + +function getNonExpandedValue(option: ValueOptions) { + const { op, varName, varValue: value, isFirst, reserved } = option; + const vals: string[] = []; + const first = getFirstOrSep(op, isFirst); + const [named, ifEmpty] = getNamedAndIfEmpty(op); + if (named && varName) { + vals.push(encodeValue(varName, reserved, op)); + if (value === "") { + if (!ifEmpty) { + vals.push(ifEmpty); + } + return !vals.join("") ? undefined : `${first}${vals.join("")}`; + } + vals.push("="); + } + + const items = []; + if (Array.isArray(value)) { + for (const val of value.filter(isDefined)) { + items.push(encodeValue(val, reserved, op)); + } + } else if (typeof value === "object") { + for (const key of Object.keys(value)) { + if (!isDefined(value[key])) { + continue; + } + items.push(encodeRFC3986URIComponent(key)); + items.push(encodeValue(value[key], reserved, op)); + } + } + vals.push(items.join(",")); + return !vals.join(",") ? undefined : `${first}${vals.join("")}`; +} + +function getVarValue(option: ValueOptions): string | undefined { + const { op, varName, modifier, isFirst, reserved, varValue: value } = option; + + if (!isDefined(value)) { + return undefined; + } else if (["string", "number", "boolean"].includes(typeof value)) { + let val = value.toString(); + const [named, ifEmpty] = getNamedAndIfEmpty(op); + const vals: string[] = [getFirstOrSep(op, isFirst)]; + if (named && varName) { + vals.push(encodeRFC3986URIComponent(varName)); + val === "" ? vals.push(ifEmpty) : vals.push("="); + } + if (modifier && modifier !== "*") { + val = val.substring(0, parseInt(modifier, 10)); + } + vals.push(encodeValue(val, reserved, op)); + return vals.join(""); + } else if (modifier === "*") { + return getExpandedValue(option); + } else { + return getNonExpandedValue(option); + } +} + +// --------------------------------------------------------------------------------------------------- +// This is an implementation of RFC 6570 URI Template: https://datatracker.ietf.org/doc/html/rfc6570. +// --------------------------------------------------------------------------------------------------- +export function expandUrlTemplate( + template: string, + context: Record, + option?: UrlTemplateOptions, +): string { + return template.replace(/\{([^\{\}]+)\}|([^\{\}]+)/g, (_, expr, text) => { + if (!expr) { + return encodeWithReserved(text); + } + let op; + if (["+", "#", ".", "/", ";", "?", "&"].includes(expr[0])) { + (op = expr[0]), (expr = expr.slice(1)); + } + const varList = expr.split(/,/g); + const result = []; + for (const varSpec of varList) { + const varMatch = /([^:\*]*)(?::(\d+)|(\*))?/.exec(varSpec); + if (!varMatch || !varMatch[1]) { + continue; + } + const varValue = getVarValue({ + isFirst: result.length === 0, + op, + varValue: context[varMatch[1]], + varName: varMatch[1], + modifier: varMatch[2] || varMatch[3], + reserved: option?.allowReserved ?? false, + }); + if (varValue) { + result.push(varValue); + } + } + return result.join(""); + }); +} diff --git a/packages/typespec-test/test/eventgrid_modular/generated/typespec-ts/src/api/operations.ts b/packages/typespec-test/test/eventgrid_modular/generated/typespec-ts/src/api/operations.ts index a204297726..09dd1f0b0f 100644 --- a/packages/typespec-test/test/eventgrid_modular/generated/typespec-ts/src/api/operations.ts +++ b/packages/typespec-test/test/eventgrid_modular/generated/typespec-ts/src/api/operations.ts @@ -31,6 +31,7 @@ import { rejectResultDeserializer, cloudEventArraySerializer, } from "../models/models.js"; +import { expandUrlTemplate } from "../static-helpers/urlTemplate.js"; import { StreamableMethod, PathUncheckedResponse, @@ -46,8 +47,17 @@ export function _publishCloudEventSend( }, options: PublishCloudEventOptionalParams = { requestOptions: {} }, ): StreamableMethod { + const path = expandUrlTemplate( + "/topics/{topicName}:publish{?api-version}", + { + topicName: topicName, + }, + { + allowReserved: options?.requestOptions?.skipUrlEncoding, + }, + ); return context - .path("/topics/{topicName}:publish", topicName) + .path(path) .post({ ...operationOptionsToRequestParameters(options), contentType: @@ -92,8 +102,17 @@ export function _publishCloudEventsSend( events: CloudEvent[], options: PublishCloudEventsOptionalParams = { requestOptions: {} }, ): StreamableMethod { + const path = expandUrlTemplate( + "/topics/{topicName}:publish{?api-version}", + { + topicName: topicName, + }, + { + allowReserved: options?.requestOptions?.skipUrlEncoding, + }, + ); return context - .path("/topics/{topicName}:publish", topicName) + .path(path) .post({ ...operationOptionsToRequestParameters(options), contentType: @@ -136,19 +155,21 @@ export function _receiveCloudEventsSend( eventSubscriptionName: string, options: ReceiveCloudEventsOptionalParams = { requestOptions: {} }, ): StreamableMethod { + const path = expandUrlTemplate( + "/topics/{topicName}/eventsubscriptions/{eventSubscriptionName}:receive{?api-version,maxEvents,maxWaitTime}", + { + topicName: topicName, + eventSubscriptionName: eventSubscriptionName, + maxEvents: options?.maxEvents, + maxWaitTime: options?.maxWaitTime, + }, + { + allowReserved: options?.requestOptions?.skipUrlEncoding, + }, + ); return context - .path( - "/topics/{topicName}/eventsubscriptions/{eventSubscriptionName}:receive", - topicName, - eventSubscriptionName, - ) - .post({ - ...operationOptionsToRequestParameters(options), - queryParameters: { - maxEvents: options?.maxEvents, - maxWaitTime: options?.maxWaitTime, - }, - }); + .path(path) + .post({ ...operationOptionsToRequestParameters(options) }); } export async function _receiveCloudEventsDeserialize( @@ -185,12 +206,18 @@ export function _acknowledgeCloudEventsSend( lockTokens: AcknowledgeOptions, options: AcknowledgeCloudEventsOptionalParams = { requestOptions: {} }, ): StreamableMethod { + const path = expandUrlTemplate( + "/topics/{topicName}/eventsubscriptions/{eventSubscriptionName}:acknowledge{?api-version}", + { + topicName: topicName, + eventSubscriptionName: eventSubscriptionName, + }, + { + allowReserved: options?.requestOptions?.skipUrlEncoding, + }, + ); return context - .path( - "/topics/{topicName}/eventsubscriptions/{eventSubscriptionName}:acknowledge", - topicName, - eventSubscriptionName, - ) + .path(path) .post({ ...operationOptionsToRequestParameters(options), contentType: @@ -235,12 +262,18 @@ export function _releaseCloudEventsSend( lockTokens: ReleaseOptions, options: ReleaseCloudEventsOptionalParams = { requestOptions: {} }, ): StreamableMethod { + const path = expandUrlTemplate( + "/topics/{topicName}/eventsubscriptions/{eventSubscriptionName}:release{?api-version}", + { + topicName: topicName, + eventSubscriptionName: eventSubscriptionName, + }, + { + allowReserved: options?.requestOptions?.skipUrlEncoding, + }, + ); return context - .path( - "/topics/{topicName}/eventsubscriptions/{eventSubscriptionName}:release", - topicName, - eventSubscriptionName, - ) + .path(path) .post({ ...operationOptionsToRequestParameters(options), contentType: @@ -285,12 +318,18 @@ export function _rejectCloudEventsSend( lockTokens: RejectOptions, options: RejectCloudEventsOptionalParams = { requestOptions: {} }, ): StreamableMethod { + const path = expandUrlTemplate( + "/topics/{topicName}/eventsubscriptions/{eventSubscriptionName}:reject{?api-version}", + { + topicName: topicName, + eventSubscriptionName: eventSubscriptionName, + }, + { + allowReserved: options?.requestOptions?.skipUrlEncoding, + }, + ); return context - .path( - "/topics/{topicName}/eventsubscriptions/{eventSubscriptionName}:reject", - topicName, - eventSubscriptionName, - ) + .path(path) .post({ ...operationOptionsToRequestParameters(options), contentType: diff --git a/packages/typespec-test/test/eventgrid_modular/generated/typespec-ts/src/static-helpers/urlTemplate.ts b/packages/typespec-test/test/eventgrid_modular/generated/typespec-ts/src/static-helpers/urlTemplate.ts new file mode 100644 index 0000000000..3d56a97a31 --- /dev/null +++ b/packages/typespec-test/test/eventgrid_modular/generated/typespec-ts/src/static-helpers/urlTemplate.ts @@ -0,0 +1,199 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +//--------------------- +// interfaces +//--------------------- +interface ValueOptions { + isFirst: boolean; // is first value in the expression + op?: string; // operator + varValue?: any; // variable value + varName?: string; // variable name + modifier?: string; // modifier e.g * + reserved: boolean; // if true we'll keep reserved words with not encoding +} + +export interface UrlTemplateOptions { + // if set to true, reserved characters will not be encoded + allowReserved?: boolean; +} + +// --------------------- +// helpers +// --------------------- +function encodeValue(val: string, reserved: boolean, op?: string) { + return reserved === true || op === "+" || op === "#" + ? encodeWithReserved(val) + : encodeRFC3986URIComponent(val); +} + +function encodeWithReserved(str: string) { + return str + .split(/(%[0-9A-Fa-f]{2})/g) + .map((part) => (!/%[0-9A-Fa-f]/.test(part) ? encodeURI(part) : part)) + .join(""); +} + +function encodeRFC3986URIComponent(str: string) { + return encodeURIComponent(str).replace( + /[!'()*]/g, + (c) => `%${c.charCodeAt(0).toString(16).toUpperCase()}`, + ); +} + +function isDefined(val: any) { + return val !== undefined && val !== null; +} + +function getNamedAndIfEmpty(op?: string): [boolean, string] { + return [ + !!op && [";", "?", "&"].includes(op), + !!op && ["?", "&"].includes(op) ? "=" : "", + ]; +} + +function getFirstOrSep(op?: string, isFirst = false) { + if (isFirst) { + return !op || op === "+" ? "" : op; + } else if (!op || op === "+" || op === "#") { + return ","; + } else if (op === "?") { + return "&"; + } else { + return op; + } +} + +function getExpandedValue(option: ValueOptions) { + let isFirst = option.isFirst; + const { op, varName, varValue: value, reserved } = option; + const vals: string[] = []; + const [named, ifEmpty] = getNamedAndIfEmpty(op); + + if (Array.isArray(value)) { + for (const val of value.filter(isDefined)) { + // prepare the following parts: separator, varName, value + vals.push(`${getFirstOrSep(op, isFirst)}`); + if (named && varName) { + vals.push(`${encodeURIComponent(varName)}`); + val === "" ? vals.push(ifEmpty) : vals.push("="); + } + vals.push(encodeValue(val, reserved, op)); + isFirst = false; + } + } else if (typeof value === "object") { + for (const key of Object.keys(value)) { + const val = value[key]; + if (!isDefined(val)) { + continue; + } + // prepare the following parts: separator, key, value + vals.push(`${getFirstOrSep(op, isFirst)}`); + if (key) { + vals.push(`${encodeURIComponent(key)}`); + named && val === "" ? vals.push(ifEmpty) : vals.push("="); + } + vals.push(encodeValue(val, reserved, op)); + isFirst = false; + } + } + return vals.join(""); +} + +function getNonExpandedValue(option: ValueOptions) { + const { op, varName, varValue: value, isFirst, reserved } = option; + const vals: string[] = []; + const first = getFirstOrSep(op, isFirst); + const [named, ifEmpty] = getNamedAndIfEmpty(op); + if (named && varName) { + vals.push(encodeValue(varName, reserved, op)); + if (value === "") { + if (!ifEmpty) { + vals.push(ifEmpty); + } + return !vals.join("") ? undefined : `${first}${vals.join("")}`; + } + vals.push("="); + } + + const items = []; + if (Array.isArray(value)) { + for (const val of value.filter(isDefined)) { + items.push(encodeValue(val, reserved, op)); + } + } else if (typeof value === "object") { + for (const key of Object.keys(value)) { + if (!isDefined(value[key])) { + continue; + } + items.push(encodeRFC3986URIComponent(key)); + items.push(encodeValue(value[key], reserved, op)); + } + } + vals.push(items.join(",")); + return !vals.join(",") ? undefined : `${first}${vals.join("")}`; +} + +function getVarValue(option: ValueOptions): string | undefined { + const { op, varName, modifier, isFirst, reserved, varValue: value } = option; + + if (!isDefined(value)) { + return undefined; + } else if (["string", "number", "boolean"].includes(typeof value)) { + let val = value.toString(); + const [named, ifEmpty] = getNamedAndIfEmpty(op); + const vals: string[] = [getFirstOrSep(op, isFirst)]; + if (named && varName) { + vals.push(encodeRFC3986URIComponent(varName)); + val === "" ? vals.push(ifEmpty) : vals.push("="); + } + if (modifier && modifier !== "*") { + val = val.substring(0, parseInt(modifier, 10)); + } + vals.push(encodeValue(val, reserved, op)); + return vals.join(""); + } else if (modifier === "*") { + return getExpandedValue(option); + } else { + return getNonExpandedValue(option); + } +} + +// --------------------------------------------------------------------------------------------------- +// This is an implementation of RFC 6570 URI Template: https://datatracker.ietf.org/doc/html/rfc6570. +// --------------------------------------------------------------------------------------------------- +export function expandUrlTemplate( + template: string, + context: Record, + option?: UrlTemplateOptions, +): string { + return template.replace(/\{([^\{\}]+)\}|([^\{\}]+)/g, (_, expr, text) => { + if (!expr) { + return encodeWithReserved(text); + } + let op; + if (["+", "#", ".", "/", ";", "?", "&"].includes(expr[0])) { + (op = expr[0]), (expr = expr.slice(1)); + } + const varList = expr.split(/,/g); + const result = []; + for (const varSpec of varList) { + const varMatch = /([^:\*]*)(?::(\d+)|(\*))?/.exec(varSpec); + if (!varMatch || !varMatch[1]) { + continue; + } + const varValue = getVarValue({ + isFirst: result.length === 0, + op, + varValue: context[varMatch[1]], + varName: varMatch[1], + modifier: varMatch[2] || varMatch[3], + reserved: option?.allowReserved ?? false, + }); + if (varValue) { + result.push(varValue); + } + } + return result.join(""); + }); +} diff --git a/packages/typespec-test/test/loadtesting_modular/generated/typespec-ts/src/loadTestAdministration/api/operations.ts b/packages/typespec-test/test/loadtesting_modular/generated/typespec-ts/src/loadTestAdministration/api/operations.ts index c5da4f8cf1..3b511e414f 100644 --- a/packages/typespec-test/test/loadtesting_modular/generated/typespec-ts/src/loadTestAdministration/api/operations.ts +++ b/packages/typespec-test/test/loadtesting_modular/generated/typespec-ts/src/loadTestAdministration/api/operations.ts @@ -37,6 +37,7 @@ import { PagedAsyncIterableIterator, buildPagedAsyncIterator, } from "../../static-helpers/pagingHelpers.js"; +import { expandUrlTemplate } from "../../static-helpers/urlTemplate.js"; import { StreamableMethod, PathUncheckedResponse, @@ -50,8 +51,17 @@ export function _createOrUpdateTestSend( body: Test, options: CreateOrUpdateTestOptionalParams = { requestOptions: {} }, ): StreamableMethod { + const path = expandUrlTemplate( + "/tests/{testId}{?api-version}", + { + testId: testId, + }, + { + allowReserved: options?.requestOptions?.skipUrlEncoding, + }, + ); return context - .path("/tests/{testId}", testId) + .path(path) .patch({ ...operationOptionsToRequestParameters(options), contentType: @@ -88,8 +98,17 @@ export function _createOrUpdateAppComponentsSend( body: TestAppComponents, options: CreateOrUpdateAppComponentsOptionalParams = { requestOptions: {} }, ): StreamableMethod { + const path = expandUrlTemplate( + "/tests/{testId}/app-components{?api-version}", + { + testId: testId, + }, + { + allowReserved: options?.requestOptions?.skipUrlEncoding, + }, + ); return context - .path("/tests/{testId}/app-components", testId) + .path(path) .patch({ ...operationOptionsToRequestParameters(options), contentType: @@ -133,8 +152,17 @@ export function _createOrUpdateServerMetricsConfigSend( requestOptions: {}, }, ): StreamableMethod { + const path = expandUrlTemplate( + "/tests/{testId}/server-metrics-config{?api-version}", + { + testId: testId, + }, + { + allowReserved: options?.requestOptions?.skipUrlEncoding, + }, + ); return context - .path("/tests/{testId}/server-metrics-config", testId) + .path(path) .patch({ ...operationOptionsToRequestParameters(options), contentType: @@ -177,8 +205,17 @@ export function _getAppComponentsSend( testId: string, options: GetAppComponentsOptionalParams = { requestOptions: {} }, ): StreamableMethod { + const path = expandUrlTemplate( + "/tests/{testId}/app-components{?api-version}", + { + testId: testId, + }, + { + allowReserved: options?.requestOptions?.skipUrlEncoding, + }, + ); return context - .path("/tests/{testId}/app-components", testId) + .path(path) .get({ ...operationOptionsToRequestParameters(options) }); } @@ -208,8 +245,17 @@ export function _getServerMetricsConfigSend( testId: string, options: GetServerMetricsConfigOptionalParams = { requestOptions: {} }, ): StreamableMethod { + const path = expandUrlTemplate( + "/tests/{testId}/server-metrics-config{?api-version}", + { + testId: testId, + }, + { + allowReserved: options?.requestOptions?.skipUrlEncoding, + }, + ); return context - .path("/tests/{testId}/server-metrics-config", testId) + .path(path) .get({ ...operationOptionsToRequestParameters(options) }); } @@ -239,8 +285,17 @@ export function _getTestSend( testId: string, options: GetTestOptionalParams = { requestOptions: {} }, ): StreamableMethod { + const path = expandUrlTemplate( + "/tests/{testId}{?api-version}", + { + testId: testId, + }, + { + allowReserved: options?.requestOptions?.skipUrlEncoding, + }, + ); return context - .path("/tests/{testId}", testId) + .path(path) .get({ ...operationOptionsToRequestParameters(options) }); } @@ -271,8 +326,18 @@ export function _getTestFileSend( fileName: string, options: GetTestFileOptionalParams = { requestOptions: {} }, ): StreamableMethod { + const path = expandUrlTemplate( + "/tests/{testId}/files/{fileName}{?api-version}", + { + testId: testId, + fileName: fileName, + }, + { + allowReserved: options?.requestOptions?.skipUrlEncoding, + }, + ); return context - .path("/tests/{testId}/files/{fileName}", testId, fileName) + .path(path) .get({ ...operationOptionsToRequestParameters(options) }); } @@ -303,8 +368,17 @@ export function _listTestFilesSend( testId: string, options: ListTestFilesOptionalParams = { requestOptions: {} }, ): StreamableMethod { + const path = expandUrlTemplate( + "/tests/{testId}/files{?api-version}", + { + testId: testId, + }, + { + allowReserved: options?.requestOptions?.skipUrlEncoding, + }, + ); return context - .path("/tests/{testId}/files", testId) + .path(path) .get({ ...operationOptionsToRequestParameters(options) }); } @@ -338,18 +412,22 @@ export function _listTestsSend( context: Client, options: ListTestsOptionalParams = { requestOptions: {} }, ): StreamableMethod { + const path = expandUrlTemplate( + "/tests{?api-version,orderby,search,lastModifiedStartTime,lastModifiedEndTime,maxpagesize}", + { + orderby: options?.orderby, + search: options?.search, + lastModifiedStartTime: options?.lastModifiedStartTime?.toISOString(), + lastModifiedEndTime: options?.lastModifiedEndTime?.toISOString(), + maxpagesize: options?.maxpagesize, + }, + { + allowReserved: options?.requestOptions?.skipUrlEncoding, + }, + ); return context - .path("/tests") - .get({ - ...operationOptionsToRequestParameters(options), - queryParameters: { - orderby: options?.orderby, - search: options?.search, - lastModifiedStartTime: options?.lastModifiedStartTime?.toISOString(), - lastModifiedEndTime: options?.lastModifiedEndTime?.toISOString(), - maxpagesize: options?.maxpagesize, - }, - }); + .path(path) + .get({ ...operationOptionsToRequestParameters(options) }); } export async function _listTestsDeserialize( @@ -387,12 +465,22 @@ export function _uploadTestFileSend( body: Uint8Array, options: UploadTestFileOptionalParams = { requestOptions: {} }, ): StreamableMethod { + const path = expandUrlTemplate( + "/tests/{testId}/files/{fileName}{?api-version,fileType}", + { + testId: testId, + fileName: fileName, + fileType: options?.fileType, + }, + { + allowReserved: options?.requestOptions?.skipUrlEncoding, + }, + ); return context - .path("/tests/{testId}/files/{fileName}", testId, fileName) + .path(path) .put({ ...operationOptionsToRequestParameters(options), contentType: (options.contentType as any) ?? "application/octet-stream", - queryParameters: { fileType: options?.fileType }, body: body, }); } @@ -436,8 +524,18 @@ export function _deleteTestFileSend( fileName: string, options: DeleteTestFileOptionalParams = { requestOptions: {} }, ): StreamableMethod { + const path = expandUrlTemplate( + "/tests/{testId}/files/{fileName}{?api-version}", + { + testId: testId, + fileName: fileName, + }, + { + allowReserved: options?.requestOptions?.skipUrlEncoding, + }, + ); return context - .path("/tests/{testId}/files/{fileName}", testId, fileName) + .path(path) .delete({ ...operationOptionsToRequestParameters(options) }); } @@ -468,8 +566,17 @@ export function _deleteTestSend( testId: string, options: DeleteTestOptionalParams = { requestOptions: {} }, ): StreamableMethod { + const path = expandUrlTemplate( + "/tests/{testId}{?api-version}", + { + testId: testId, + }, + { + allowReserved: options?.requestOptions?.skipUrlEncoding, + }, + ); return context - .path("/tests/{testId}", testId) + .path(path) .delete({ ...operationOptionsToRequestParameters(options) }); } diff --git a/packages/typespec-test/test/loadtesting_modular/generated/typespec-ts/src/loadTestRun/api/operations.ts b/packages/typespec-test/test/loadtesting_modular/generated/typespec-ts/src/loadTestRun/api/operations.ts index 4f9e14838f..f002dc72d5 100644 --- a/packages/typespec-test/test/loadtesting_modular/generated/typespec-ts/src/loadTestRun/api/operations.ts +++ b/packages/typespec-test/test/loadtesting_modular/generated/typespec-ts/src/loadTestRun/api/operations.ts @@ -47,6 +47,7 @@ import { PagedAsyncIterableIterator, buildPagedAsyncIterator, } from "../../static-helpers/pagingHelpers.js"; +import { expandUrlTemplate } from "../../static-helpers/urlTemplate.js"; import { StreamableMethod, PathUncheckedResponse, @@ -60,13 +61,22 @@ export function _createOrUpdateTestRunSend( body: TestRun, options: CreateOrUpdateTestRunOptionalParams = { requestOptions: {} }, ): StreamableMethod { + const path = expandUrlTemplate( + "/test-runs/{testRunId}{?api-version,oldTestRunId}", + { + testRunId: testRunId, + oldTestRunId: options?.oldTestRunId, + }, + { + allowReserved: options?.requestOptions?.skipUrlEncoding, + }, + ); return context - .path("/test-runs/{testRunId}", testRunId) + .path(path) .patch({ ...operationOptionsToRequestParameters(options), contentType: (options.contentType as any) ?? "application/merge-patch+json", - queryParameters: { oldTestRunId: options?.oldTestRunId }, body: testRunSerializer(body), }); } @@ -104,8 +114,17 @@ export function _createOrUpdateAppComponentsSend( body: TestRunAppComponents, options: CreateOrUpdateAppComponentsOptionalParams = { requestOptions: {} }, ): StreamableMethod { + const path = expandUrlTemplate( + "/test-runs/{testRunId}/app-components{?api-version}", + { + testRunId: testRunId, + }, + { + allowReserved: options?.requestOptions?.skipUrlEncoding, + }, + ); return context - .path("/test-runs/{testRunId}/app-components", testRunId) + .path(path) .patch({ ...operationOptionsToRequestParameters(options), contentType: @@ -149,8 +168,17 @@ export function _createOrUpdateServerMetricsConfigSend( requestOptions: {}, }, ): StreamableMethod { + const path = expandUrlTemplate( + "/test-runs/{testRunId}/server-metrics-config{?api-version}", + { + testRunId: testRunId, + }, + { + allowReserved: options?.requestOptions?.skipUrlEncoding, + }, + ); return context - .path("/test-runs/{testRunId}/server-metrics-config", testRunId) + .path(path) .patch({ ...operationOptionsToRequestParameters(options), contentType: @@ -193,8 +221,17 @@ export function _deleteTestRunSend( testRunId: string, options: DeleteTestRunOptionalParams = { requestOptions: {} }, ): StreamableMethod { + const path = expandUrlTemplate( + "/test-runs/{testRunId}{?api-version}", + { + testRunId: testRunId, + }, + { + allowReserved: options?.requestOptions?.skipUrlEncoding, + }, + ); return context - .path("/test-runs/{testRunId}", testRunId) + .path(path) .delete({ ...operationOptionsToRequestParameters(options) }); } @@ -224,8 +261,17 @@ export function _getAppComponentsSend( testRunId: string, options: GetAppComponentsOptionalParams = { requestOptions: {} }, ): StreamableMethod { + const path = expandUrlTemplate( + "/test-runs/{testRunId}/app-components{?api-version}", + { + testRunId: testRunId, + }, + { + allowReserved: options?.requestOptions?.skipUrlEncoding, + }, + ); return context - .path("/test-runs/{testRunId}/app-components", testRunId) + .path(path) .get({ ...operationOptionsToRequestParameters(options) }); } @@ -258,8 +304,17 @@ export function _getServerMetricsConfigSend( testRunId: string, options: GetServerMetricsConfigOptionalParams = { requestOptions: {} }, ): StreamableMethod { + const path = expandUrlTemplate( + "/test-runs/{testRunId}/server-metrics-config{?api-version}", + { + testRunId: testRunId, + }, + { + allowReserved: options?.requestOptions?.skipUrlEncoding, + }, + ); return context - .path("/test-runs/{testRunId}/server-metrics-config", testRunId) + .path(path) .get({ ...operationOptionsToRequestParameters(options) }); } @@ -289,8 +344,17 @@ export function _getTestRunSend( testRunId: string, options: GetTestRunOptionalParams = { requestOptions: {} }, ): StreamableMethod { + const path = expandUrlTemplate( + "/test-runs/{testRunId}{?api-version}", + { + testRunId: testRunId, + }, + { + allowReserved: options?.requestOptions?.skipUrlEncoding, + }, + ); return context - .path("/test-runs/{testRunId}", testRunId) + .path(path) .get({ ...operationOptionsToRequestParameters(options) }); } @@ -321,8 +385,18 @@ export function _getTestRunFileSend( fileName: string, options: GetTestRunFileOptionalParams = { requestOptions: {} }, ): StreamableMethod { + const path = expandUrlTemplate( + "/test-runs/{testRunId}/files/{fileName}{?api-version}", + { + testRunId: testRunId, + fileName: fileName, + }, + { + allowReserved: options?.requestOptions?.skipUrlEncoding, + }, + ); return context - .path("/test-runs/{testRunId}/files/{fileName}", testRunId, fileName) + .path(path) .get({ ...operationOptionsToRequestParameters(options) }); } @@ -362,21 +436,23 @@ export function _listMetricDimensionValuesSend( timespan: string, options: ListMetricDimensionValuesOptionalParams = { requestOptions: {} }, ): StreamableMethod { + const path = expandUrlTemplate( + "/test-runs/{testRunId}/metric-dimensions/{name}/values{?api-version,metricname,interval,metricNamespace,timespan}", + { + testRunId: testRunId, + name: name, + metricname: metricname, + interval: options?.interval, + metricNamespace: metricNamespace, + timespan: timespan, + }, + { + allowReserved: options?.requestOptions?.skipUrlEncoding, + }, + ); return context - .path( - "/test-runs/{testRunId}/metric-dimensions/{name}/values", - testRunId, - name, - ) - .get({ - ...operationOptionsToRequestParameters(options), - queryParameters: { - metricname: metricname, - interval: options?.interval, - metricNamespace: metricNamespace, - timespan: timespan, - }, - }); + .path(path) + .get({ ...operationOptionsToRequestParameters(options) }); } export async function _listMetricDimensionValuesDeserialize( @@ -418,12 +494,19 @@ export function _listMetricDefinitionsSend( metricNamespace: string, options: ListMetricDefinitionsOptionalParams = { requestOptions: {} }, ): StreamableMethod { + const path = expandUrlTemplate( + "/test-runs/{testRunId}/metric-definitions{?api-version,metricNamespace}", + { + testRunId: testRunId, + metricNamespace: metricNamespace, + }, + { + allowReserved: options?.requestOptions?.skipUrlEncoding, + }, + ); return context - .path("/test-runs/{testRunId}/metric-definitions", testRunId) - .get({ - ...operationOptionsToRequestParameters(options), - queryParameters: { metricNamespace: metricNamespace }, - }); + .path(path) + .get({ ...operationOptionsToRequestParameters(options) }); } export async function _listMetricDefinitionsDeserialize( @@ -458,8 +541,17 @@ export function _listMetricNamespacesSend( testRunId: string, options: ListMetricNamespacesOptionalParams = { requestOptions: {} }, ): StreamableMethod { + const path = expandUrlTemplate( + "/test-runs/{testRunId}/metric-namespaces{?api-version}", + { + testRunId: testRunId, + }, + { + allowReserved: options?.requestOptions?.skipUrlEncoding, + }, + ); return context - .path("/test-runs/{testRunId}/metric-namespaces", testRunId) + .path(path) .get({ ...operationOptionsToRequestParameters(options) }); } @@ -492,17 +584,24 @@ export function _listMetricsSend( timespan: string, options: ListMetricsOptionalParams = { requestOptions: {} }, ): StreamableMethod { + const path = expandUrlTemplate( + "/test-runs/{testRunId}/metrics{?api-version,aggregation,metricname,interval,metricNamespace,timespan}", + { + testRunId: testRunId, + aggregation: options?.aggregation, + metricname: metricname, + interval: options?.interval, + metricNamespace: metricNamespace, + timespan: timespan, + }, + { + allowReserved: options?.requestOptions?.skipUrlEncoding, + }, + ); return context - .path("/test-runs/{testRunId}/metrics", testRunId) + .path(path) .post({ ...operationOptionsToRequestParameters(options), - queryParameters: { - aggregation: options?.aggregation, - metricname: metricname, - interval: options?.interval, - metricNamespace: metricNamespace, - timespan: timespan, - }, body: !options["body"] ? options["body"] : metricRequestPayloadSerializer(options["body"]), @@ -550,20 +649,24 @@ export function _listTestRunsSend( context: Client, options: ListTestRunsOptionalParams = { requestOptions: {} }, ): StreamableMethod { + const path = expandUrlTemplate( + "/test-runs{?api-version,orderby,search,testId,executionFrom,executionTo,status,maxpagesize}", + { + orderby: options?.orderby, + search: options?.search, + testId: options?.testId, + executionFrom: options?.executionFrom?.toISOString(), + executionTo: options?.executionTo?.toISOString(), + status: options?.status, + maxpagesize: options?.maxpagesize, + }, + { + allowReserved: options?.requestOptions?.skipUrlEncoding, + }, + ); return context - .path("/test-runs") - .get({ - ...operationOptionsToRequestParameters(options), - queryParameters: { - orderby: options?.orderby, - search: options?.search, - testId: options?.testId, - executionFrom: options?.executionFrom?.toISOString(), - executionTo: options?.executionTo?.toISOString(), - status: options?.status, - maxpagesize: options?.maxpagesize, - }, - }); + .path(path) + .get({ ...operationOptionsToRequestParameters(options) }); } export async function _listTestRunsDeserialize( @@ -596,8 +699,17 @@ export function _stopTestRunSend( testRunId: string, options: StopTestRunOptionalParams = { requestOptions: {} }, ): StreamableMethod { + const path = expandUrlTemplate( + "/test-runs/{testRunId}:stop{?api-version}", + { + testRunId: testRunId, + }, + { + allowReserved: options?.requestOptions?.skipUrlEncoding, + }, + ); return context - .path("/test-runs/{testRunId}:stop", testRunId) + .path(path) .post({ ...operationOptionsToRequestParameters(options) }); } diff --git a/packages/typespec-test/test/loadtesting_modular/generated/typespec-ts/src/static-helpers/urlTemplate.ts b/packages/typespec-test/test/loadtesting_modular/generated/typespec-ts/src/static-helpers/urlTemplate.ts new file mode 100644 index 0000000000..3d56a97a31 --- /dev/null +++ b/packages/typespec-test/test/loadtesting_modular/generated/typespec-ts/src/static-helpers/urlTemplate.ts @@ -0,0 +1,199 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +//--------------------- +// interfaces +//--------------------- +interface ValueOptions { + isFirst: boolean; // is first value in the expression + op?: string; // operator + varValue?: any; // variable value + varName?: string; // variable name + modifier?: string; // modifier e.g * + reserved: boolean; // if true we'll keep reserved words with not encoding +} + +export interface UrlTemplateOptions { + // if set to true, reserved characters will not be encoded + allowReserved?: boolean; +} + +// --------------------- +// helpers +// --------------------- +function encodeValue(val: string, reserved: boolean, op?: string) { + return reserved === true || op === "+" || op === "#" + ? encodeWithReserved(val) + : encodeRFC3986URIComponent(val); +} + +function encodeWithReserved(str: string) { + return str + .split(/(%[0-9A-Fa-f]{2})/g) + .map((part) => (!/%[0-9A-Fa-f]/.test(part) ? encodeURI(part) : part)) + .join(""); +} + +function encodeRFC3986URIComponent(str: string) { + return encodeURIComponent(str).replace( + /[!'()*]/g, + (c) => `%${c.charCodeAt(0).toString(16).toUpperCase()}`, + ); +} + +function isDefined(val: any) { + return val !== undefined && val !== null; +} + +function getNamedAndIfEmpty(op?: string): [boolean, string] { + return [ + !!op && [";", "?", "&"].includes(op), + !!op && ["?", "&"].includes(op) ? "=" : "", + ]; +} + +function getFirstOrSep(op?: string, isFirst = false) { + if (isFirst) { + return !op || op === "+" ? "" : op; + } else if (!op || op === "+" || op === "#") { + return ","; + } else if (op === "?") { + return "&"; + } else { + return op; + } +} + +function getExpandedValue(option: ValueOptions) { + let isFirst = option.isFirst; + const { op, varName, varValue: value, reserved } = option; + const vals: string[] = []; + const [named, ifEmpty] = getNamedAndIfEmpty(op); + + if (Array.isArray(value)) { + for (const val of value.filter(isDefined)) { + // prepare the following parts: separator, varName, value + vals.push(`${getFirstOrSep(op, isFirst)}`); + if (named && varName) { + vals.push(`${encodeURIComponent(varName)}`); + val === "" ? vals.push(ifEmpty) : vals.push("="); + } + vals.push(encodeValue(val, reserved, op)); + isFirst = false; + } + } else if (typeof value === "object") { + for (const key of Object.keys(value)) { + const val = value[key]; + if (!isDefined(val)) { + continue; + } + // prepare the following parts: separator, key, value + vals.push(`${getFirstOrSep(op, isFirst)}`); + if (key) { + vals.push(`${encodeURIComponent(key)}`); + named && val === "" ? vals.push(ifEmpty) : vals.push("="); + } + vals.push(encodeValue(val, reserved, op)); + isFirst = false; + } + } + return vals.join(""); +} + +function getNonExpandedValue(option: ValueOptions) { + const { op, varName, varValue: value, isFirst, reserved } = option; + const vals: string[] = []; + const first = getFirstOrSep(op, isFirst); + const [named, ifEmpty] = getNamedAndIfEmpty(op); + if (named && varName) { + vals.push(encodeValue(varName, reserved, op)); + if (value === "") { + if (!ifEmpty) { + vals.push(ifEmpty); + } + return !vals.join("") ? undefined : `${first}${vals.join("")}`; + } + vals.push("="); + } + + const items = []; + if (Array.isArray(value)) { + for (const val of value.filter(isDefined)) { + items.push(encodeValue(val, reserved, op)); + } + } else if (typeof value === "object") { + for (const key of Object.keys(value)) { + if (!isDefined(value[key])) { + continue; + } + items.push(encodeRFC3986URIComponent(key)); + items.push(encodeValue(value[key], reserved, op)); + } + } + vals.push(items.join(",")); + return !vals.join(",") ? undefined : `${first}${vals.join("")}`; +} + +function getVarValue(option: ValueOptions): string | undefined { + const { op, varName, modifier, isFirst, reserved, varValue: value } = option; + + if (!isDefined(value)) { + return undefined; + } else if (["string", "number", "boolean"].includes(typeof value)) { + let val = value.toString(); + const [named, ifEmpty] = getNamedAndIfEmpty(op); + const vals: string[] = [getFirstOrSep(op, isFirst)]; + if (named && varName) { + vals.push(encodeRFC3986URIComponent(varName)); + val === "" ? vals.push(ifEmpty) : vals.push("="); + } + if (modifier && modifier !== "*") { + val = val.substring(0, parseInt(modifier, 10)); + } + vals.push(encodeValue(val, reserved, op)); + return vals.join(""); + } else if (modifier === "*") { + return getExpandedValue(option); + } else { + return getNonExpandedValue(option); + } +} + +// --------------------------------------------------------------------------------------------------- +// This is an implementation of RFC 6570 URI Template: https://datatracker.ietf.org/doc/html/rfc6570. +// --------------------------------------------------------------------------------------------------- +export function expandUrlTemplate( + template: string, + context: Record, + option?: UrlTemplateOptions, +): string { + return template.replace(/\{([^\{\}]+)\}|([^\{\}]+)/g, (_, expr, text) => { + if (!expr) { + return encodeWithReserved(text); + } + let op; + if (["+", "#", ".", "/", ";", "?", "&"].includes(expr[0])) { + (op = expr[0]), (expr = expr.slice(1)); + } + const varList = expr.split(/,/g); + const result = []; + for (const varSpec of varList) { + const varMatch = /([^:\*]*)(?::(\d+)|(\*))?/.exec(varSpec); + if (!varMatch || !varMatch[1]) { + continue; + } + const varValue = getVarValue({ + isFirst: result.length === 0, + op, + varValue: context[varMatch[1]], + varName: varMatch[1], + modifier: varMatch[2] || varMatch[3], + reserved: option?.allowReserved ?? false, + }); + if (varValue) { + result.push(varValue); + } + } + return result.join(""); + }); +} diff --git a/packages/typespec-test/test/loadtesting_modular/generated/typespec-ts/src/testProfileAdministration/api/operations.ts b/packages/typespec-test/test/loadtesting_modular/generated/typespec-ts/src/testProfileAdministration/api/operations.ts index 20e50d48a3..93f86c7445 100644 --- a/packages/typespec-test/test/loadtesting_modular/generated/typespec-ts/src/testProfileAdministration/api/operations.ts +++ b/packages/typespec-test/test/loadtesting_modular/generated/typespec-ts/src/testProfileAdministration/api/operations.ts @@ -19,6 +19,7 @@ import { PagedAsyncIterableIterator, buildPagedAsyncIterator, } from "../../static-helpers/pagingHelpers.js"; +import { expandUrlTemplate } from "../../static-helpers/urlTemplate.js"; import { StreamableMethod, PathUncheckedResponse, @@ -32,8 +33,17 @@ export function _createOrUpdateTestProfileSend( body: TestProfile, options: CreateOrUpdateTestProfileOptionalParams = { requestOptions: {} }, ): StreamableMethod { + const path = expandUrlTemplate( + "/test-profiles/{testProfileId}{?api-version}", + { + testProfileId: testProfileId, + }, + { + allowReserved: options?.requestOptions?.skipUrlEncoding, + }, + ); return context - .path("/test-profiles/{testProfileId}", testProfileId) + .path(path) .patch({ ...operationOptionsToRequestParameters(options), contentType: @@ -74,8 +84,17 @@ export function _deleteTestProfileSend( testProfileId: string, options: DeleteTestProfileOptionalParams = { requestOptions: {} }, ): StreamableMethod { + const path = expandUrlTemplate( + "/test-profiles/{testProfileId}{?api-version}", + { + testProfileId: testProfileId, + }, + { + allowReserved: options?.requestOptions?.skipUrlEncoding, + }, + ); return context - .path("/test-profiles/{testProfileId}", testProfileId) + .path(path) .delete({ ...operationOptionsToRequestParameters(options) }); } @@ -105,8 +124,17 @@ export function _getTestProfileSend( testProfileId: string, options: GetTestProfileOptionalParams = { requestOptions: {} }, ): StreamableMethod { + const path = expandUrlTemplate( + "/test-profiles/{testProfileId}{?api-version}", + { + testProfileId: testProfileId, + }, + { + allowReserved: options?.requestOptions?.skipUrlEncoding, + }, + ); return context - .path("/test-profiles/{testProfileId}", testProfileId) + .path(path) .get({ ...operationOptionsToRequestParameters(options) }); } @@ -135,18 +163,22 @@ export function _listTestProfilesSend( context: Client, options: ListTestProfilesOptionalParams = { requestOptions: {} }, ): StreamableMethod { + const path = expandUrlTemplate( + "/test-profiles{?api-version,maxpagesize,lastModifiedStartTime,lastModifiedEndTime,testProfileIds,testIds}", + { + maxpagesize: options?.maxpagesize, + lastModifiedStartTime: options?.lastModifiedStartTime?.toISOString(), + lastModifiedEndTime: options?.lastModifiedEndTime?.toISOString(), + testProfileIds: options?.testProfileIds, + testIds: options?.testIds, + }, + { + allowReserved: options?.requestOptions?.skipUrlEncoding, + }, + ); return context - .path("/test-profiles") - .get({ - ...operationOptionsToRequestParameters(options), - queryParameters: { - maxpagesize: options?.maxpagesize, - lastModifiedStartTime: options?.lastModifiedStartTime?.toISOString(), - lastModifiedEndTime: options?.lastModifiedEndTime?.toISOString(), - testProfileIds: options?.testProfileIds, - testIds: options?.testIds, - }, - }); + .path(path) + .get({ ...operationOptionsToRequestParameters(options) }); } export async function _listTestProfilesDeserialize( diff --git a/packages/typespec-test/test/loadtesting_modular/generated/typespec-ts/src/testProfileRun/api/operations.ts b/packages/typespec-test/test/loadtesting_modular/generated/typespec-ts/src/testProfileRun/api/operations.ts index 865818a82f..9457cda010 100644 --- a/packages/typespec-test/test/loadtesting_modular/generated/typespec-ts/src/testProfileRun/api/operations.ts +++ b/packages/typespec-test/test/loadtesting_modular/generated/typespec-ts/src/testProfileRun/api/operations.ts @@ -20,6 +20,7 @@ import { PagedAsyncIterableIterator, buildPagedAsyncIterator, } from "../../static-helpers/pagingHelpers.js"; +import { expandUrlTemplate } from "../../static-helpers/urlTemplate.js"; import { StreamableMethod, PathUncheckedResponse, @@ -33,8 +34,17 @@ export function _createOrUpdateTestProfileRunSend( body: TestProfileRun, options: CreateOrUpdateTestProfileRunOptionalParams = { requestOptions: {} }, ): StreamableMethod { + const path = expandUrlTemplate( + "/test-profile-runs/{testProfileRunId}{?api-version}", + { + testProfileRunId: testProfileRunId, + }, + { + allowReserved: options?.requestOptions?.skipUrlEncoding, + }, + ); return context - .path("/test-profile-runs/{testProfileRunId}", testProfileRunId) + .path(path) .patch({ ...operationOptionsToRequestParameters(options), contentType: @@ -75,8 +85,17 @@ export function _deleteTestProfileRunSend( testProfileRunId: string, options: DeleteTestProfileRunOptionalParams = { requestOptions: {} }, ): StreamableMethod { + const path = expandUrlTemplate( + "/test-profile-runs/{testProfileRunId}{?api-version}", + { + testProfileRunId: testProfileRunId, + }, + { + allowReserved: options?.requestOptions?.skipUrlEncoding, + }, + ); return context - .path("/test-profile-runs/{testProfileRunId}", testProfileRunId) + .path(path) .delete({ ...operationOptionsToRequestParameters(options) }); } @@ -110,8 +129,17 @@ export function _getTestProfileRunSend( testProfileRunId: string, options: GetTestProfileRunOptionalParams = { requestOptions: {} }, ): StreamableMethod { + const path = expandUrlTemplate( + "/test-profile-runs/{testProfileRunId}{?api-version}", + { + testProfileRunId: testProfileRunId, + }, + { + allowReserved: options?.requestOptions?.skipUrlEncoding, + }, + ); return context - .path("/test-profile-runs/{testProfileRunId}", testProfileRunId) + .path(path) .get({ ...operationOptionsToRequestParameters(options) }); } @@ -144,23 +172,27 @@ export function _listTestProfileRunsSend( context: Client, options: ListTestProfileRunsOptionalParams = { requestOptions: {} }, ): StreamableMethod { + const path = expandUrlTemplate( + "/test-profile-runs{?api-version,maxpagesize,minStartDateTime,maxStartDateTime,minEndDateTime,maxEndDateTime,createdDateStartTime,createdDateEndTime,testProfileRunIds,testProfileIds,statuses}", + { + maxpagesize: options?.maxpagesize, + minStartDateTime: options?.minStartDateTime?.toISOString(), + maxStartDateTime: options?.maxStartDateTime?.toISOString(), + minEndDateTime: options?.minEndDateTime?.toISOString(), + maxEndDateTime: options?.maxEndDateTime?.toISOString(), + createdDateStartTime: options?.createdDateStartTime?.toISOString(), + createdDateEndTime: options?.createdDateEndTime?.toISOString(), + testProfileRunIds: options?.testProfileRunIds, + testProfileIds: options?.testProfileIds, + statuses: options?.statuses, + }, + { + allowReserved: options?.requestOptions?.skipUrlEncoding, + }, + ); return context - .path("/test-profile-runs") - .get({ - ...operationOptionsToRequestParameters(options), - queryParameters: { - maxpagesize: options?.maxpagesize, - minStartDateTime: options?.minStartDateTime?.toISOString(), - maxStartDateTime: options?.maxStartDateTime?.toISOString(), - minEndDateTime: options?.minEndDateTime?.toISOString(), - maxEndDateTime: options?.maxEndDateTime?.toISOString(), - createdDateStartTime: options?.createdDateStartTime?.toISOString(), - createdDateEndTime: options?.createdDateEndTime?.toISOString(), - testProfileRunIds: options?.testProfileRunIds, - testProfileIds: options?.testProfileIds, - statuses: options?.statuses, - }, - }); + .path(path) + .get({ ...operationOptionsToRequestParameters(options) }); } export async function _listTestProfileRunsDeserialize( @@ -193,8 +225,17 @@ export function _stopTestProfileRunSend( testProfileRunId: string, options: StopTestProfileRunOptionalParams = { requestOptions: {} }, ): StreamableMethod { + const path = expandUrlTemplate( + "/test-profile-runs/{testProfileRunId}:stop{?api-version}", + { + testProfileRunId: testProfileRunId, + }, + { + allowReserved: options?.requestOptions?.skipUrlEncoding, + }, + ); return context - .path("/test-profile-runs/{testProfileRunId}:stop", testProfileRunId) + .path(path) .post({ ...operationOptionsToRequestParameters(options) }); } diff --git a/packages/typespec-test/test/openai_generic/generated/typespec-ts/src/api/files/index.ts b/packages/typespec-test/test/openai_generic/generated/typespec-ts/src/api/files/index.ts index 899412257a..fb7bb03280 100644 --- a/packages/typespec-test/test/openai_generic/generated/typespec-ts/src/api/files/index.ts +++ b/packages/typespec-test/test/openai_generic/generated/typespec-ts/src/api/files/index.ts @@ -19,6 +19,7 @@ import { DeleteFileResponse, deleteFileResponseDeserializer, } from "../../models/models.js"; +import { expandUrlTemplate } from "../../static-helpers/urlTemplate.js"; import { StreamableMethod, PathUncheckedResponse, @@ -93,8 +94,17 @@ export function _retrieveSend( fileId: string, options: FilesRetrieveOptionalParams = { requestOptions: {} }, ): StreamableMethod { + const path = expandUrlTemplate( + "/files/files/{file_id}", + { + fileId: fileId, + }, + { + allowReserved: options?.requestOptions?.skipUrlEncoding, + }, + ); return context - .path("/files/files/{file_id}", fileId) + .path(path) .post({ ...operationOptionsToRequestParameters(options) }); } @@ -123,8 +133,17 @@ export function _$deleteSend( fileId: string, options: FilesDeleteOptionalParams = { requestOptions: {} }, ): StreamableMethod { + const path = expandUrlTemplate( + "/files/files/{file_id}", + { + fileId: fileId, + }, + { + allowReserved: options?.requestOptions?.skipUrlEncoding, + }, + ); return context - .path("/files/files/{file_id}", fileId) + .path(path) .delete({ ...operationOptionsToRequestParameters(options) }); } @@ -158,8 +177,17 @@ export function _downloadSend( fileId: string, options: FilesDownloadOptionalParams = { requestOptions: {} }, ): StreamableMethod { + const path = expandUrlTemplate( + "/files/files/{file_id}/content", + { + fileId: fileId, + }, + { + allowReserved: options?.requestOptions?.skipUrlEncoding, + }, + ); return context - .path("/files/files/{file_id}/content", fileId) + .path(path) .get({ ...operationOptionsToRequestParameters(options) }); } diff --git a/packages/typespec-test/test/openai_generic/generated/typespec-ts/src/api/fineTunes/index.ts b/packages/typespec-test/test/openai_generic/generated/typespec-ts/src/api/fineTunes/index.ts index aa5de3fc20..689a9becb4 100644 --- a/packages/typespec-test/test/openai_generic/generated/typespec-ts/src/api/fineTunes/index.ts +++ b/packages/typespec-test/test/openai_generic/generated/typespec-ts/src/api/fineTunes/index.ts @@ -19,6 +19,7 @@ import { ListFineTuneEventsResponse, listFineTuneEventsResponseDeserializer, } from "../../models/models.js"; +import { expandUrlTemplate } from "../../static-helpers/urlTemplate.js"; import { StreamableMethod, PathUncheckedResponse, @@ -92,8 +93,17 @@ export function _retrieveSend( fineTuneId: string, options: FineTunesRetrieveOptionalParams = { requestOptions: {} }, ): StreamableMethod { + const path = expandUrlTemplate( + "/fine-tunes/{fine_tune_id}", + { + fineTuneId: fineTuneId, + }, + { + allowReserved: options?.requestOptions?.skipUrlEncoding, + }, + ); return context - .path("/fine-tunes/{fine_tune_id}", fineTuneId) + .path(path) .get({ ...operationOptionsToRequestParameters(options) }); } @@ -122,12 +132,19 @@ export function _listEventsSend( fineTuneId: string, options: FineTunesListEventsOptionalParams = { requestOptions: {} }, ): StreamableMethod { + const path = expandUrlTemplate( + "/fine-tunes/{fine_tune_id}/events{?stream}", + { + fineTuneId: fineTuneId, + stream: options?.stream, + }, + { + allowReserved: options?.requestOptions?.skipUrlEncoding, + }, + ); return context - .path("/fine-tunes/{fine_tune_id}/events", fineTuneId) - .get({ - ...operationOptionsToRequestParameters(options), - queryParameters: { stream: options?.stream }, - }); + .path(path) + .get({ ...operationOptionsToRequestParameters(options) }); } export async function _listEventsDeserialize( @@ -155,8 +172,17 @@ export function _cancelSend( fineTuneId: string, options: FineTunesCancelOptionalParams = { requestOptions: {} }, ): StreamableMethod { + const path = expandUrlTemplate( + "/fine-tunes/{fine_tune_id}/cancel", + { + fineTuneId: fineTuneId, + }, + { + allowReserved: options?.requestOptions?.skipUrlEncoding, + }, + ); return context - .path("/fine-tunes/{fine_tune_id}/cancel", fineTuneId) + .path(path) .post({ ...operationOptionsToRequestParameters(options) }); } diff --git a/packages/typespec-test/test/openai_generic/generated/typespec-ts/src/api/fineTuning/jobs/index.ts b/packages/typespec-test/test/openai_generic/generated/typespec-ts/src/api/fineTuning/jobs/index.ts index 07d9c5e064..5f9ca5ca4d 100644 --- a/packages/typespec-test/test/openai_generic/generated/typespec-ts/src/api/fineTuning/jobs/index.ts +++ b/packages/typespec-test/test/openai_generic/generated/typespec-ts/src/api/fineTuning/jobs/index.ts @@ -19,6 +19,7 @@ import { ListFineTuningJobEventsResponse, listFineTuningJobEventsResponseDeserializer, } from "../../../models/models.js"; +import { expandUrlTemplate } from "../../../static-helpers/urlTemplate.js"; import { StreamableMethod, PathUncheckedResponse, @@ -71,12 +72,19 @@ export function _listSend( context: Client, options: FineTuningJobsListOptionalParams = { requestOptions: {} }, ): StreamableMethod { + const path = expandUrlTemplate( + "/fine_tuning/jobs{?after,limit}", + { + after: options?.after, + limit: options?.limit, + }, + { + allowReserved: options?.requestOptions?.skipUrlEncoding, + }, + ); return context - .path("/fine_tuning/jobs") - .get({ - ...operationOptionsToRequestParameters(options), - queryParameters: { after: options?.after, limit: options?.limit }, - }); + .path(path) + .get({ ...operationOptionsToRequestParameters(options) }); } export async function _listDeserialize( @@ -103,8 +111,17 @@ export function _retrieveSend( fineTuningJobId: string, options: FineTuningJobsRetrieveOptionalParams = { requestOptions: {} }, ): StreamableMethod { + const path = expandUrlTemplate( + "/fine_tuning/jobs/{fine_tuning_job_id}", + { + fineTuningJobId: fineTuningJobId, + }, + { + allowReserved: options?.requestOptions?.skipUrlEncoding, + }, + ); return context - .path("/fine_tuning/jobs/{fine_tuning_job_id}", fineTuningJobId) + .path(path) .get({ ...operationOptionsToRequestParameters(options) }); } @@ -133,12 +150,20 @@ export function _listEventsSend( fineTuningJobId: string, options: FineTuningJobsListEventsOptionalParams = { requestOptions: {} }, ): StreamableMethod { + const path = expandUrlTemplate( + "/fine_tuning/jobs/{fine_tuning_job_id}/events{?after,limit}", + { + fineTuningJobId: fineTuningJobId, + after: options?.after, + limit: options?.limit, + }, + { + allowReserved: options?.requestOptions?.skipUrlEncoding, + }, + ); return context - .path("/fine_tuning/jobs/{fine_tuning_job_id}/events", fineTuningJobId) - .get({ - ...operationOptionsToRequestParameters(options), - queryParameters: { after: options?.after, limit: options?.limit }, - }); + .path(path) + .get({ ...operationOptionsToRequestParameters(options) }); } export async function _listEventsDeserialize( @@ -166,8 +191,17 @@ export function _cancelSend( fineTuningJobId: string, options: FineTuningJobsCancelOptionalParams = { requestOptions: {} }, ): StreamableMethod { + const path = expandUrlTemplate( + "/fine_tuning/jobs/{fine_tuning_job_id}/cancel", + { + fineTuningJobId: fineTuningJobId, + }, + { + allowReserved: options?.requestOptions?.skipUrlEncoding, + }, + ); return context - .path("/fine_tuning/jobs/{fine_tuning_job_id}/cancel", fineTuningJobId) + .path(path) .post({ ...operationOptionsToRequestParameters(options) }); } diff --git a/packages/typespec-test/test/openai_generic/generated/typespec-ts/src/api/models/index.ts b/packages/typespec-test/test/openai_generic/generated/typespec-ts/src/api/models/index.ts index 1192f91414..4f9d4739de 100644 --- a/packages/typespec-test/test/openai_generic/generated/typespec-ts/src/api/models/index.ts +++ b/packages/typespec-test/test/openai_generic/generated/typespec-ts/src/api/models/index.ts @@ -15,6 +15,7 @@ import { DeleteModelResponse, deleteModelResponseDeserializer, } from "../../models/models.js"; +import { expandUrlTemplate } from "../../static-helpers/urlTemplate.js"; import { StreamableMethod, PathUncheckedResponse, @@ -55,8 +56,17 @@ export function _retrieveSend( model: string, options: ModelsRetrieveOptionalParams = { requestOptions: {} }, ): StreamableMethod { + const path = expandUrlTemplate( + "/models/{model}", + { + model: model, + }, + { + allowReserved: options?.requestOptions?.skipUrlEncoding, + }, + ); return context - .path("/models/{model}", model) + .path(path) .get({ ...operationOptionsToRequestParameters(options) }); } @@ -85,8 +95,17 @@ export function _$deleteSend( model: string, options: ModelsDeleteOptionalParams = { requestOptions: {} }, ): StreamableMethod { + const path = expandUrlTemplate( + "/models/{model}", + { + model: model, + }, + { + allowReserved: options?.requestOptions?.skipUrlEncoding, + }, + ); return context - .path("/models/{model}", model) + .path(path) .delete({ ...operationOptionsToRequestParameters(options) }); } diff --git a/packages/typespec-test/test/openai_generic/generated/typespec-ts/src/static-helpers/urlTemplate.ts b/packages/typespec-test/test/openai_generic/generated/typespec-ts/src/static-helpers/urlTemplate.ts new file mode 100644 index 0000000000..3d56a97a31 --- /dev/null +++ b/packages/typespec-test/test/openai_generic/generated/typespec-ts/src/static-helpers/urlTemplate.ts @@ -0,0 +1,199 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +//--------------------- +// interfaces +//--------------------- +interface ValueOptions { + isFirst: boolean; // is first value in the expression + op?: string; // operator + varValue?: any; // variable value + varName?: string; // variable name + modifier?: string; // modifier e.g * + reserved: boolean; // if true we'll keep reserved words with not encoding +} + +export interface UrlTemplateOptions { + // if set to true, reserved characters will not be encoded + allowReserved?: boolean; +} + +// --------------------- +// helpers +// --------------------- +function encodeValue(val: string, reserved: boolean, op?: string) { + return reserved === true || op === "+" || op === "#" + ? encodeWithReserved(val) + : encodeRFC3986URIComponent(val); +} + +function encodeWithReserved(str: string) { + return str + .split(/(%[0-9A-Fa-f]{2})/g) + .map((part) => (!/%[0-9A-Fa-f]/.test(part) ? encodeURI(part) : part)) + .join(""); +} + +function encodeRFC3986URIComponent(str: string) { + return encodeURIComponent(str).replace( + /[!'()*]/g, + (c) => `%${c.charCodeAt(0).toString(16).toUpperCase()}`, + ); +} + +function isDefined(val: any) { + return val !== undefined && val !== null; +} + +function getNamedAndIfEmpty(op?: string): [boolean, string] { + return [ + !!op && [";", "?", "&"].includes(op), + !!op && ["?", "&"].includes(op) ? "=" : "", + ]; +} + +function getFirstOrSep(op?: string, isFirst = false) { + if (isFirst) { + return !op || op === "+" ? "" : op; + } else if (!op || op === "+" || op === "#") { + return ","; + } else if (op === "?") { + return "&"; + } else { + return op; + } +} + +function getExpandedValue(option: ValueOptions) { + let isFirst = option.isFirst; + const { op, varName, varValue: value, reserved } = option; + const vals: string[] = []; + const [named, ifEmpty] = getNamedAndIfEmpty(op); + + if (Array.isArray(value)) { + for (const val of value.filter(isDefined)) { + // prepare the following parts: separator, varName, value + vals.push(`${getFirstOrSep(op, isFirst)}`); + if (named && varName) { + vals.push(`${encodeURIComponent(varName)}`); + val === "" ? vals.push(ifEmpty) : vals.push("="); + } + vals.push(encodeValue(val, reserved, op)); + isFirst = false; + } + } else if (typeof value === "object") { + for (const key of Object.keys(value)) { + const val = value[key]; + if (!isDefined(val)) { + continue; + } + // prepare the following parts: separator, key, value + vals.push(`${getFirstOrSep(op, isFirst)}`); + if (key) { + vals.push(`${encodeURIComponent(key)}`); + named && val === "" ? vals.push(ifEmpty) : vals.push("="); + } + vals.push(encodeValue(val, reserved, op)); + isFirst = false; + } + } + return vals.join(""); +} + +function getNonExpandedValue(option: ValueOptions) { + const { op, varName, varValue: value, isFirst, reserved } = option; + const vals: string[] = []; + const first = getFirstOrSep(op, isFirst); + const [named, ifEmpty] = getNamedAndIfEmpty(op); + if (named && varName) { + vals.push(encodeValue(varName, reserved, op)); + if (value === "") { + if (!ifEmpty) { + vals.push(ifEmpty); + } + return !vals.join("") ? undefined : `${first}${vals.join("")}`; + } + vals.push("="); + } + + const items = []; + if (Array.isArray(value)) { + for (const val of value.filter(isDefined)) { + items.push(encodeValue(val, reserved, op)); + } + } else if (typeof value === "object") { + for (const key of Object.keys(value)) { + if (!isDefined(value[key])) { + continue; + } + items.push(encodeRFC3986URIComponent(key)); + items.push(encodeValue(value[key], reserved, op)); + } + } + vals.push(items.join(",")); + return !vals.join(",") ? undefined : `${first}${vals.join("")}`; +} + +function getVarValue(option: ValueOptions): string | undefined { + const { op, varName, modifier, isFirst, reserved, varValue: value } = option; + + if (!isDefined(value)) { + return undefined; + } else if (["string", "number", "boolean"].includes(typeof value)) { + let val = value.toString(); + const [named, ifEmpty] = getNamedAndIfEmpty(op); + const vals: string[] = [getFirstOrSep(op, isFirst)]; + if (named && varName) { + vals.push(encodeRFC3986URIComponent(varName)); + val === "" ? vals.push(ifEmpty) : vals.push("="); + } + if (modifier && modifier !== "*") { + val = val.substring(0, parseInt(modifier, 10)); + } + vals.push(encodeValue(val, reserved, op)); + return vals.join(""); + } else if (modifier === "*") { + return getExpandedValue(option); + } else { + return getNonExpandedValue(option); + } +} + +// --------------------------------------------------------------------------------------------------- +// This is an implementation of RFC 6570 URI Template: https://datatracker.ietf.org/doc/html/rfc6570. +// --------------------------------------------------------------------------------------------------- +export function expandUrlTemplate( + template: string, + context: Record, + option?: UrlTemplateOptions, +): string { + return template.replace(/\{([^\{\}]+)\}|([^\{\}]+)/g, (_, expr, text) => { + if (!expr) { + return encodeWithReserved(text); + } + let op; + if (["+", "#", ".", "/", ";", "?", "&"].includes(expr[0])) { + (op = expr[0]), (expr = expr.slice(1)); + } + const varList = expr.split(/,/g); + const result = []; + for (const varSpec of varList) { + const varMatch = /([^:\*]*)(?::(\d+)|(\*))?/.exec(varSpec); + if (!varMatch || !varMatch[1]) { + continue; + } + const varValue = getVarValue({ + isFirst: result.length === 0, + op, + varValue: context[varMatch[1]], + varName: varMatch[1], + modifier: varMatch[2] || varMatch[3], + reserved: option?.allowReserved ?? false, + }); + if (varValue) { + result.push(varValue); + } + } + return result.join(""); + }); +} diff --git a/packages/typespec-test/test/openai_modular/generated/typespec-ts/src/api/operations.ts b/packages/typespec-test/test/openai_modular/generated/typespec-ts/src/api/operations.ts index 76da673e8d..95fb2a5dd0 100644 --- a/packages/typespec-test/test/openai_modular/generated/typespec-ts/src/api/operations.ts +++ b/packages/typespec-test/test/openai_modular/generated/typespec-ts/src/api/operations.ts @@ -41,6 +41,7 @@ import { Embeddings, embeddingsDeserializer, } from "../models/models.js"; +import { expandUrlTemplate } from "../static-helpers/urlTemplate.js"; import { StreamableMethod, PathUncheckedResponse, @@ -56,8 +57,17 @@ export function _getAudioTranscriptionAsPlainTextSend( requestOptions: {}, }, ): StreamableMethod { + const path = expandUrlTemplate( + "/deployments/{deploymentId}/audio/transcriptions{?api-version}", + { + deploymentId: deploymentId, + }, + { + allowReserved: options?.requestOptions?.skipUrlEncoding, + }, + ); return context - .path("/deployments/{deploymentId}/audio/transcriptions", deploymentId) + .path(path) .post({ ...operationOptionsToRequestParameters(options), contentType: (options.contentType as any) ?? "multipart/form-data", @@ -105,8 +115,17 @@ export function _getAudioTranscriptionAsResponseObjectSend( requestOptions: {}, }, ): StreamableMethod { + const path = expandUrlTemplate( + "/deployments/{deploymentId}/audio/transcriptions{?api-version}", + { + deploymentId: deploymentId, + }, + { + allowReserved: options?.requestOptions?.skipUrlEncoding, + }, + ); return context - .path("/deployments/{deploymentId}/audio/transcriptions", deploymentId) + .path(path) .post({ ...operationOptionsToRequestParameters(options), contentType: (options.contentType as any) ?? "multipart/form-data", @@ -154,8 +173,17 @@ export function _getAudioTranslationAsPlainTextSend( requestOptions: {}, }, ): StreamableMethod { + const path = expandUrlTemplate( + "/deployments/{deploymentId}/audio/translations{?api-version}", + { + deploymentId: deploymentId, + }, + { + allowReserved: options?.requestOptions?.skipUrlEncoding, + }, + ); return context - .path("/deployments/{deploymentId}/audio/translations", deploymentId) + .path(path) .post({ ...operationOptionsToRequestParameters(options), contentType: (options.contentType as any) ?? "multipart/form-data", @@ -200,8 +228,17 @@ export function _getAudioTranslationAsResponseObjectSend( requestOptions: {}, }, ): StreamableMethod { + const path = expandUrlTemplate( + "/deployments/{deploymentId}/audio/translations{?api-version}", + { + deploymentId: deploymentId, + }, + { + allowReserved: options?.requestOptions?.skipUrlEncoding, + }, + ); return context - .path("/deployments/{deploymentId}/audio/translations", deploymentId) + .path(path) .post({ ...operationOptionsToRequestParameters(options), contentType: (options.contentType as any) ?? "multipart/form-data", @@ -244,8 +281,17 @@ export function _getCompletionsSend( body: CompletionsOptions, options: GetCompletionsOptionalParams = { requestOptions: {} }, ): StreamableMethod { + const path = expandUrlTemplate( + "/deployments/{deploymentId}/completions{?api-version}", + { + deploymentId: deploymentId, + }, + { + allowReserved: options?.requestOptions?.skipUrlEncoding, + }, + ); return context - .path("/deployments/{deploymentId}/completions", deploymentId) + .path(path) .post({ ...operationOptionsToRequestParameters(options), body: completionsOptionsSerializer(body), @@ -289,8 +335,17 @@ export function _getChatCompletionsSend( body: ChatCompletionsOptions, options: GetChatCompletionsOptionalParams = { requestOptions: {} }, ): StreamableMethod { + const path = expandUrlTemplate( + "/deployments/{deploymentId}/chat/completions{?api-version}", + { + deploymentId: deploymentId, + }, + { + allowReserved: options?.requestOptions?.skipUrlEncoding, + }, + ); return context - .path("/deployments/{deploymentId}/chat/completions", deploymentId) + .path(path) .post({ ...operationOptionsToRequestParameters(options), body: chatCompletionsOptionsSerializer(body), @@ -334,8 +389,17 @@ export function _getImageGenerationsSend( body: ImageGenerationOptions, options: GetImageGenerationsOptionalParams = { requestOptions: {} }, ): StreamableMethod { + const path = expandUrlTemplate( + "/deployments/{deploymentId}/images/generations{?api-version}", + { + deploymentId: deploymentId, + }, + { + allowReserved: options?.requestOptions?.skipUrlEncoding, + }, + ); return context - .path("/deployments/{deploymentId}/images/generations", deploymentId) + .path(path) .post({ ...operationOptionsToRequestParameters(options), body: imageGenerationOptionsSerializer(body), @@ -375,8 +439,17 @@ export function _generateSpeechFromTextSend( body: SpeechGenerationOptions, options: GenerateSpeechFromTextOptionalParams = { requestOptions: {} }, ): StreamableMethod { + const path = expandUrlTemplate( + "/deployments/{deploymentId}/audio/speech{?api-version}", + { + deploymentId: deploymentId, + }, + { + allowReserved: options?.requestOptions?.skipUrlEncoding, + }, + ); return context - .path("/deployments/{deploymentId}/audio/speech", deploymentId) + .path(path) .post({ ...operationOptionsToRequestParameters(options), body: speechGenerationOptionsSerializer(body), @@ -416,8 +489,17 @@ export function _getEmbeddingsSend( body: EmbeddingsOptions, options: GetEmbeddingsOptionalParams = { requestOptions: {} }, ): StreamableMethod { + const path = expandUrlTemplate( + "/deployments/{deploymentId}/embeddings{?api-version}", + { + deploymentId: deploymentId, + }, + { + allowReserved: options?.requestOptions?.skipUrlEncoding, + }, + ); return context - .path("/deployments/{deploymentId}/embeddings", deploymentId) + .path(path) .post({ ...operationOptionsToRequestParameters(options), body: embeddingsOptionsSerializer(body), diff --git a/packages/typespec-test/test/openai_modular/generated/typespec-ts/src/static-helpers/urlTemplate.ts b/packages/typespec-test/test/openai_modular/generated/typespec-ts/src/static-helpers/urlTemplate.ts new file mode 100644 index 0000000000..3d56a97a31 --- /dev/null +++ b/packages/typespec-test/test/openai_modular/generated/typespec-ts/src/static-helpers/urlTemplate.ts @@ -0,0 +1,199 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +//--------------------- +// interfaces +//--------------------- +interface ValueOptions { + isFirst: boolean; // is first value in the expression + op?: string; // operator + varValue?: any; // variable value + varName?: string; // variable name + modifier?: string; // modifier e.g * + reserved: boolean; // if true we'll keep reserved words with not encoding +} + +export interface UrlTemplateOptions { + // if set to true, reserved characters will not be encoded + allowReserved?: boolean; +} + +// --------------------- +// helpers +// --------------------- +function encodeValue(val: string, reserved: boolean, op?: string) { + return reserved === true || op === "+" || op === "#" + ? encodeWithReserved(val) + : encodeRFC3986URIComponent(val); +} + +function encodeWithReserved(str: string) { + return str + .split(/(%[0-9A-Fa-f]{2})/g) + .map((part) => (!/%[0-9A-Fa-f]/.test(part) ? encodeURI(part) : part)) + .join(""); +} + +function encodeRFC3986URIComponent(str: string) { + return encodeURIComponent(str).replace( + /[!'()*]/g, + (c) => `%${c.charCodeAt(0).toString(16).toUpperCase()}`, + ); +} + +function isDefined(val: any) { + return val !== undefined && val !== null; +} + +function getNamedAndIfEmpty(op?: string): [boolean, string] { + return [ + !!op && [";", "?", "&"].includes(op), + !!op && ["?", "&"].includes(op) ? "=" : "", + ]; +} + +function getFirstOrSep(op?: string, isFirst = false) { + if (isFirst) { + return !op || op === "+" ? "" : op; + } else if (!op || op === "+" || op === "#") { + return ","; + } else if (op === "?") { + return "&"; + } else { + return op; + } +} + +function getExpandedValue(option: ValueOptions) { + let isFirst = option.isFirst; + const { op, varName, varValue: value, reserved } = option; + const vals: string[] = []; + const [named, ifEmpty] = getNamedAndIfEmpty(op); + + if (Array.isArray(value)) { + for (const val of value.filter(isDefined)) { + // prepare the following parts: separator, varName, value + vals.push(`${getFirstOrSep(op, isFirst)}`); + if (named && varName) { + vals.push(`${encodeURIComponent(varName)}`); + val === "" ? vals.push(ifEmpty) : vals.push("="); + } + vals.push(encodeValue(val, reserved, op)); + isFirst = false; + } + } else if (typeof value === "object") { + for (const key of Object.keys(value)) { + const val = value[key]; + if (!isDefined(val)) { + continue; + } + // prepare the following parts: separator, key, value + vals.push(`${getFirstOrSep(op, isFirst)}`); + if (key) { + vals.push(`${encodeURIComponent(key)}`); + named && val === "" ? vals.push(ifEmpty) : vals.push("="); + } + vals.push(encodeValue(val, reserved, op)); + isFirst = false; + } + } + return vals.join(""); +} + +function getNonExpandedValue(option: ValueOptions) { + const { op, varName, varValue: value, isFirst, reserved } = option; + const vals: string[] = []; + const first = getFirstOrSep(op, isFirst); + const [named, ifEmpty] = getNamedAndIfEmpty(op); + if (named && varName) { + vals.push(encodeValue(varName, reserved, op)); + if (value === "") { + if (!ifEmpty) { + vals.push(ifEmpty); + } + return !vals.join("") ? undefined : `${first}${vals.join("")}`; + } + vals.push("="); + } + + const items = []; + if (Array.isArray(value)) { + for (const val of value.filter(isDefined)) { + items.push(encodeValue(val, reserved, op)); + } + } else if (typeof value === "object") { + for (const key of Object.keys(value)) { + if (!isDefined(value[key])) { + continue; + } + items.push(encodeRFC3986URIComponent(key)); + items.push(encodeValue(value[key], reserved, op)); + } + } + vals.push(items.join(",")); + return !vals.join(",") ? undefined : `${first}${vals.join("")}`; +} + +function getVarValue(option: ValueOptions): string | undefined { + const { op, varName, modifier, isFirst, reserved, varValue: value } = option; + + if (!isDefined(value)) { + return undefined; + } else if (["string", "number", "boolean"].includes(typeof value)) { + let val = value.toString(); + const [named, ifEmpty] = getNamedAndIfEmpty(op); + const vals: string[] = [getFirstOrSep(op, isFirst)]; + if (named && varName) { + vals.push(encodeRFC3986URIComponent(varName)); + val === "" ? vals.push(ifEmpty) : vals.push("="); + } + if (modifier && modifier !== "*") { + val = val.substring(0, parseInt(modifier, 10)); + } + vals.push(encodeValue(val, reserved, op)); + return vals.join(""); + } else if (modifier === "*") { + return getExpandedValue(option); + } else { + return getNonExpandedValue(option); + } +} + +// --------------------------------------------------------------------------------------------------- +// This is an implementation of RFC 6570 URI Template: https://datatracker.ietf.org/doc/html/rfc6570. +// --------------------------------------------------------------------------------------------------- +export function expandUrlTemplate( + template: string, + context: Record, + option?: UrlTemplateOptions, +): string { + return template.replace(/\{([^\{\}]+)\}|([^\{\}]+)/g, (_, expr, text) => { + if (!expr) { + return encodeWithReserved(text); + } + let op; + if (["+", "#", ".", "/", ";", "?", "&"].includes(expr[0])) { + (op = expr[0]), (expr = expr.slice(1)); + } + const varList = expr.split(/,/g); + const result = []; + for (const varSpec of varList) { + const varMatch = /([^:\*]*)(?::(\d+)|(\*))?/.exec(varSpec); + if (!varMatch || !varMatch[1]) { + continue; + } + const varValue = getVarValue({ + isFirst: result.length === 0, + op, + varValue: context[varMatch[1]], + varName: varMatch[1], + modifier: varMatch[2] || varMatch[3], + reserved: option?.allowReserved ?? false, + }); + if (varValue) { + result.push(varValue); + } + } + return result.join(""); + }); +} diff --git a/packages/typespec-test/test/openai_non_branded/generated/typespec-ts/src/api/files/index.ts b/packages/typespec-test/test/openai_non_branded/generated/typespec-ts/src/api/files/index.ts index 8b00a519c6..2a5336b31d 100644 --- a/packages/typespec-test/test/openai_non_branded/generated/typespec-ts/src/api/files/index.ts +++ b/packages/typespec-test/test/openai_non_branded/generated/typespec-ts/src/api/files/index.ts @@ -18,6 +18,7 @@ import { DeleteFileResponse, deleteFileResponseDeserializer, } from "../../models/models.js"; +import { expandUrlTemplate } from "../../static-helpers/urlTemplate.js"; import { StreamableMethod, PathUncheckedResponse, @@ -92,8 +93,17 @@ export function _retrieveSend( fileId: string, options: FilesRetrieveOptionalParams = { requestOptions: {} }, ): StreamableMethod { + const path = expandUrlTemplate( + "/files/files/{file_id}", + { + fileId: fileId, + }, + { + allowReserved: options?.requestOptions?.skipUrlEncoding, + }, + ); return context - .path("/files/files/{file_id}", fileId) + .path(path) .post({ ...operationOptionsToRequestParameters(options) }); } @@ -122,8 +132,17 @@ export function _$deleteSend( fileId: string, options: FilesDeleteOptionalParams = { requestOptions: {} }, ): StreamableMethod { + const path = expandUrlTemplate( + "/files/files/{file_id}", + { + fileId: fileId, + }, + { + allowReserved: options?.requestOptions?.skipUrlEncoding, + }, + ); return context - .path("/files/files/{file_id}", fileId) + .path(path) .delete({ ...operationOptionsToRequestParameters(options) }); } @@ -157,8 +176,17 @@ export function _downloadSend( fileId: string, options: FilesDownloadOptionalParams = { requestOptions: {} }, ): StreamableMethod { + const path = expandUrlTemplate( + "/files/files/{file_id}/content", + { + fileId: fileId, + }, + { + allowReserved: options?.requestOptions?.skipUrlEncoding, + }, + ); return context - .path("/files/files/{file_id}/content", fileId) + .path(path) .get({ ...operationOptionsToRequestParameters(options) }); } diff --git a/packages/typespec-test/test/openai_non_branded/generated/typespec-ts/src/api/fineTunes/index.ts b/packages/typespec-test/test/openai_non_branded/generated/typespec-ts/src/api/fineTunes/index.ts index 75574deb85..3facbfa63f 100644 --- a/packages/typespec-test/test/openai_non_branded/generated/typespec-ts/src/api/fineTunes/index.ts +++ b/packages/typespec-test/test/openai_non_branded/generated/typespec-ts/src/api/fineTunes/index.ts @@ -18,6 +18,7 @@ import { ListFineTuneEventsResponse, listFineTuneEventsResponseDeserializer, } from "../../models/models.js"; +import { expandUrlTemplate } from "../../static-helpers/urlTemplate.js"; import { StreamableMethod, PathUncheckedResponse, @@ -91,8 +92,17 @@ export function _retrieveSend( fineTuneId: string, options: FineTunesRetrieveOptionalParams = { requestOptions: {} }, ): StreamableMethod { + const path = expandUrlTemplate( + "/fine-tunes/{fine_tune_id}", + { + fineTuneId: fineTuneId, + }, + { + allowReserved: options?.requestOptions?.skipUrlEncoding, + }, + ); return context - .path("/fine-tunes/{fine_tune_id}", fineTuneId) + .path(path) .get({ ...operationOptionsToRequestParameters(options) }); } @@ -121,12 +131,19 @@ export function _listEventsSend( fineTuneId: string, options: FineTunesListEventsOptionalParams = { requestOptions: {} }, ): StreamableMethod { + const path = expandUrlTemplate( + "/fine-tunes/{fine_tune_id}/events{?stream}", + { + fineTuneId: fineTuneId, + stream: options?.stream, + }, + { + allowReserved: options?.requestOptions?.skipUrlEncoding, + }, + ); return context - .path("/fine-tunes/{fine_tune_id}/events", fineTuneId) - .get({ - ...operationOptionsToRequestParameters(options), - queryParameters: { stream: options?.stream }, - }); + .path(path) + .get({ ...operationOptionsToRequestParameters(options) }); } export async function _listEventsDeserialize( @@ -154,8 +171,17 @@ export function _cancelSend( fineTuneId: string, options: FineTunesCancelOptionalParams = { requestOptions: {} }, ): StreamableMethod { + const path = expandUrlTemplate( + "/fine-tunes/{fine_tune_id}/cancel", + { + fineTuneId: fineTuneId, + }, + { + allowReserved: options?.requestOptions?.skipUrlEncoding, + }, + ); return context - .path("/fine-tunes/{fine_tune_id}/cancel", fineTuneId) + .path(path) .post({ ...operationOptionsToRequestParameters(options) }); } diff --git a/packages/typespec-test/test/openai_non_branded/generated/typespec-ts/src/api/fineTuning/jobs/index.ts b/packages/typespec-test/test/openai_non_branded/generated/typespec-ts/src/api/fineTuning/jobs/index.ts index 868832e3c3..43a1bd21fa 100644 --- a/packages/typespec-test/test/openai_non_branded/generated/typespec-ts/src/api/fineTuning/jobs/index.ts +++ b/packages/typespec-test/test/openai_non_branded/generated/typespec-ts/src/api/fineTuning/jobs/index.ts @@ -18,6 +18,7 @@ import { ListFineTuningJobEventsResponse, listFineTuningJobEventsResponseDeserializer, } from "../../../models/models.js"; +import { expandUrlTemplate } from "../../../static-helpers/urlTemplate.js"; import { StreamableMethod, PathUncheckedResponse, @@ -70,12 +71,19 @@ export function _listSend( context: Client, options: FineTuningJobsListOptionalParams = { requestOptions: {} }, ): StreamableMethod { + const path = expandUrlTemplate( + "/fine_tuning/jobs{?after,limit}", + { + after: options?.after, + limit: options?.limit, + }, + { + allowReserved: options?.requestOptions?.skipUrlEncoding, + }, + ); return context - .path("/fine_tuning/jobs") - .get({ - ...operationOptionsToRequestParameters(options), - queryParameters: { after: options?.after, limit: options?.limit }, - }); + .path(path) + .get({ ...operationOptionsToRequestParameters(options) }); } export async function _listDeserialize( @@ -102,8 +110,17 @@ export function _retrieveSend( fineTuningJobId: string, options: FineTuningJobsRetrieveOptionalParams = { requestOptions: {} }, ): StreamableMethod { + const path = expandUrlTemplate( + "/fine_tuning/jobs/{fine_tuning_job_id}", + { + fineTuningJobId: fineTuningJobId, + }, + { + allowReserved: options?.requestOptions?.skipUrlEncoding, + }, + ); return context - .path("/fine_tuning/jobs/{fine_tuning_job_id}", fineTuningJobId) + .path(path) .get({ ...operationOptionsToRequestParameters(options) }); } @@ -132,12 +149,20 @@ export function _listEventsSend( fineTuningJobId: string, options: FineTuningJobsListEventsOptionalParams = { requestOptions: {} }, ): StreamableMethod { + const path = expandUrlTemplate( + "/fine_tuning/jobs/{fine_tuning_job_id}/events{?after,limit}", + { + fineTuningJobId: fineTuningJobId, + after: options?.after, + limit: options?.limit, + }, + { + allowReserved: options?.requestOptions?.skipUrlEncoding, + }, + ); return context - .path("/fine_tuning/jobs/{fine_tuning_job_id}/events", fineTuningJobId) - .get({ - ...operationOptionsToRequestParameters(options), - queryParameters: { after: options?.after, limit: options?.limit }, - }); + .path(path) + .get({ ...operationOptionsToRequestParameters(options) }); } export async function _listEventsDeserialize( @@ -165,8 +190,17 @@ export function _cancelSend( fineTuningJobId: string, options: FineTuningJobsCancelOptionalParams = { requestOptions: {} }, ): StreamableMethod { + const path = expandUrlTemplate( + "/fine_tuning/jobs/{fine_tuning_job_id}/cancel", + { + fineTuningJobId: fineTuningJobId, + }, + { + allowReserved: options?.requestOptions?.skipUrlEncoding, + }, + ); return context - .path("/fine_tuning/jobs/{fine_tuning_job_id}/cancel", fineTuningJobId) + .path(path) .post({ ...operationOptionsToRequestParameters(options) }); } diff --git a/packages/typespec-test/test/openai_non_branded/generated/typespec-ts/src/api/models/index.ts b/packages/typespec-test/test/openai_non_branded/generated/typespec-ts/src/api/models/index.ts index ac1bd7ae38..3fe82ea8a9 100644 --- a/packages/typespec-test/test/openai_non_branded/generated/typespec-ts/src/api/models/index.ts +++ b/packages/typespec-test/test/openai_non_branded/generated/typespec-ts/src/api/models/index.ts @@ -14,6 +14,7 @@ import { DeleteModelResponse, deleteModelResponseDeserializer, } from "../../models/models.js"; +import { expandUrlTemplate } from "../../static-helpers/urlTemplate.js"; import { StreamableMethod, PathUncheckedResponse, @@ -54,8 +55,17 @@ export function _retrieveSend( model: string, options: ModelsRetrieveOptionalParams = { requestOptions: {} }, ): StreamableMethod { + const path = expandUrlTemplate( + "/models/{model}", + { + model: model, + }, + { + allowReserved: options?.requestOptions?.skipUrlEncoding, + }, + ); return context - .path("/models/{model}", model) + .path(path) .get({ ...operationOptionsToRequestParameters(options) }); } @@ -84,8 +94,17 @@ export function _$deleteSend( model: string, options: ModelsDeleteOptionalParams = { requestOptions: {} }, ): StreamableMethod { + const path = expandUrlTemplate( + "/models/{model}", + { + model: model, + }, + { + allowReserved: options?.requestOptions?.skipUrlEncoding, + }, + ); return context - .path("/models/{model}", model) + .path(path) .delete({ ...operationOptionsToRequestParameters(options) }); } diff --git a/packages/typespec-test/test/openai_non_branded/generated/typespec-ts/src/static-helpers/urlTemplate.ts b/packages/typespec-test/test/openai_non_branded/generated/typespec-ts/src/static-helpers/urlTemplate.ts new file mode 100644 index 0000000000..c768c23002 --- /dev/null +++ b/packages/typespec-test/test/openai_non_branded/generated/typespec-ts/src/static-helpers/urlTemplate.ts @@ -0,0 +1,198 @@ +// Licensed under the MIT License. + +//--------------------- +// interfaces +//--------------------- +interface ValueOptions { + isFirst: boolean; // is first value in the expression + op?: string; // operator + varValue?: any; // variable value + varName?: string; // variable name + modifier?: string; // modifier e.g * + reserved: boolean; // if true we'll keep reserved words with not encoding +} + +export interface UrlTemplateOptions { + // if set to true, reserved characters will not be encoded + allowReserved?: boolean; +} + +// --------------------- +// helpers +// --------------------- +function encodeValue(val: string, reserved: boolean, op?: string) { + return reserved === true || op === "+" || op === "#" + ? encodeWithReserved(val) + : encodeRFC3986URIComponent(val); +} + +function encodeWithReserved(str: string) { + return str + .split(/(%[0-9A-Fa-f]{2})/g) + .map((part) => (!/%[0-9A-Fa-f]/.test(part) ? encodeURI(part) : part)) + .join(""); +} + +function encodeRFC3986URIComponent(str: string) { + return encodeURIComponent(str).replace( + /[!'()*]/g, + (c) => `%${c.charCodeAt(0).toString(16).toUpperCase()}`, + ); +} + +function isDefined(val: any) { + return val !== undefined && val !== null; +} + +function getNamedAndIfEmpty(op?: string): [boolean, string] { + return [ + !!op && [";", "?", "&"].includes(op), + !!op && ["?", "&"].includes(op) ? "=" : "", + ]; +} + +function getFirstOrSep(op?: string, isFirst = false) { + if (isFirst) { + return !op || op === "+" ? "" : op; + } else if (!op || op === "+" || op === "#") { + return ","; + } else if (op === "?") { + return "&"; + } else { + return op; + } +} + +function getExpandedValue(option: ValueOptions) { + let isFirst = option.isFirst; + const { op, varName, varValue: value, reserved } = option; + const vals: string[] = []; + const [named, ifEmpty] = getNamedAndIfEmpty(op); + + if (Array.isArray(value)) { + for (const val of value.filter(isDefined)) { + // prepare the following parts: separator, varName, value + vals.push(`${getFirstOrSep(op, isFirst)}`); + if (named && varName) { + vals.push(`${encodeURIComponent(varName)}`); + val === "" ? vals.push(ifEmpty) : vals.push("="); + } + vals.push(encodeValue(val, reserved, op)); + isFirst = false; + } + } else if (typeof value === "object") { + for (const key of Object.keys(value)) { + const val = value[key]; + if (!isDefined(val)) { + continue; + } + // prepare the following parts: separator, key, value + vals.push(`${getFirstOrSep(op, isFirst)}`); + if (key) { + vals.push(`${encodeURIComponent(key)}`); + named && val === "" ? vals.push(ifEmpty) : vals.push("="); + } + vals.push(encodeValue(val, reserved, op)); + isFirst = false; + } + } + return vals.join(""); +} + +function getNonExpandedValue(option: ValueOptions) { + const { op, varName, varValue: value, isFirst, reserved } = option; + const vals: string[] = []; + const first = getFirstOrSep(op, isFirst); + const [named, ifEmpty] = getNamedAndIfEmpty(op); + if (named && varName) { + vals.push(encodeValue(varName, reserved, op)); + if (value === "") { + if (!ifEmpty) { + vals.push(ifEmpty); + } + return !vals.join("") ? undefined : `${first}${vals.join("")}`; + } + vals.push("="); + } + + const items = []; + if (Array.isArray(value)) { + for (const val of value.filter(isDefined)) { + items.push(encodeValue(val, reserved, op)); + } + } else if (typeof value === "object") { + for (const key of Object.keys(value)) { + if (!isDefined(value[key])) { + continue; + } + items.push(encodeRFC3986URIComponent(key)); + items.push(encodeValue(value[key], reserved, op)); + } + } + vals.push(items.join(",")); + return !vals.join(",") ? undefined : `${first}${vals.join("")}`; +} + +function getVarValue(option: ValueOptions): string | undefined { + const { op, varName, modifier, isFirst, reserved, varValue: value } = option; + + if (!isDefined(value)) { + return undefined; + } else if (["string", "number", "boolean"].includes(typeof value)) { + let val = value.toString(); + const [named, ifEmpty] = getNamedAndIfEmpty(op); + const vals: string[] = [getFirstOrSep(op, isFirst)]; + if (named && varName) { + vals.push(encodeRFC3986URIComponent(varName)); + val === "" ? vals.push(ifEmpty) : vals.push("="); + } + if (modifier && modifier !== "*") { + val = val.substring(0, parseInt(modifier, 10)); + } + vals.push(encodeValue(val, reserved, op)); + return vals.join(""); + } else if (modifier === "*") { + return getExpandedValue(option); + } else { + return getNonExpandedValue(option); + } +} + +// --------------------------------------------------------------------------------------------------- +// This is an implementation of RFC 6570 URI Template: https://datatracker.ietf.org/doc/html/rfc6570. +// --------------------------------------------------------------------------------------------------- +export function expandUrlTemplate( + template: string, + context: Record, + option?: UrlTemplateOptions, +): string { + return template.replace(/\{([^\{\}]+)\}|([^\{\}]+)/g, (_, expr, text) => { + if (!expr) { + return encodeWithReserved(text); + } + let op; + if (["+", "#", ".", "/", ";", "?", "&"].includes(expr[0])) { + (op = expr[0]), (expr = expr.slice(1)); + } + const varList = expr.split(/,/g); + const result = []; + for (const varSpec of varList) { + const varMatch = /([^:\*]*)(?::(\d+)|(\*))?/.exec(varSpec); + if (!varMatch || !varMatch[1]) { + continue; + } + const varValue = getVarValue({ + isFirst: result.length === 0, + op, + varValue: context[varMatch[1]], + varName: varMatch[1], + modifier: varMatch[2] || varMatch[3], + reserved: option?.allowReserved ?? false, + }); + if (varValue) { + result.push(varValue); + } + } + return result.join(""); + }); +} diff --git a/packages/typespec-test/test/schemaRegistry/generated/typespec-ts/src/api/schemaOperations/index.ts b/packages/typespec-test/test/schemaRegistry/generated/typespec-ts/src/api/schemaOperations/index.ts index b710d15559..a47699138d 100644 --- a/packages/typespec-test/test/schemaRegistry/generated/typespec-ts/src/api/schemaOperations/index.ts +++ b/packages/typespec-test/test/schemaRegistry/generated/typespec-ts/src/api/schemaOperations/index.ts @@ -23,6 +23,7 @@ import { PagedAsyncIterableIterator, buildPagedAsyncIterator, } from "../../static-helpers/pagingHelpers.js"; +import { expandUrlTemplate } from "../../static-helpers/urlTemplate.js"; import { StreamableMethod, PathUncheckedResponse, @@ -74,8 +75,17 @@ export function _getSchemaByIdSend( id: string, options: SchemaOperationsGetSchemaByIdOptionalParams = { requestOptions: {} }, ): StreamableMethod { + const path = expandUrlTemplate( + "/$schemaGroups/$schemas/{id}{?api-version}", + { + id: id, + }, + { + allowReserved: options?.requestOptions?.skipUrlEncoding, + }, + ); return context - .path("/$schemaGroups/$schemas/{id}", id) + .path(path) .get({ ...operationOptionsToRequestParameters(options) }); } @@ -110,8 +120,18 @@ export function _listSchemaVersionsSend( requestOptions: {}, }, ): StreamableMethod { + const path = expandUrlTemplate( + "/$schemaGroups/{groupName}/schemas/{name}/versions{?api-version}", + { + groupName: groupName, + name: name, + }, + { + allowReserved: options?.requestOptions?.skipUrlEncoding, + }, + ); return context - .path("/$schemaGroups/{groupName}/schemas/{name}/versions", groupName, name) + .path(path) .get({ ...operationOptionsToRequestParameters(options) }); } @@ -153,13 +173,19 @@ export function _getSchemaByVersionSend( requestOptions: {}, }, ): StreamableMethod { + const path = expandUrlTemplate( + "/$schemaGroups/{groupName}/schemas/{name}/versions/{schemaVersion}{?api-version}", + { + groupName: groupName, + name: name, + schemaVersion: schemaVersion, + }, + { + allowReserved: options?.requestOptions?.skipUrlEncoding, + }, + ); return context - .path( - "/$schemaGroups/{groupName}/schemas/{name}/versions/{schemaVersion}", - groupName, - name, - schemaVersion, - ) + .path(path) .get({ ...operationOptionsToRequestParameters(options) }); } @@ -206,8 +232,18 @@ export function _getSchemaIdByContentSend( requestOptions: {}, }, ): StreamableMethod { + const path = expandUrlTemplate( + "/$schemaGroups/{groupName}/schemas/{name}:get-id{?api-version}", + { + groupName: groupName, + name: name, + }, + { + allowReserved: options?.requestOptions?.skipUrlEncoding, + }, + ); return context - .path("/$schemaGroups/{groupName}/schemas/{name}:get-id", groupName, name) + .path(path) .post({ ...operationOptionsToRequestParameters(options), contentType: contentType, @@ -258,8 +294,18 @@ export function _registerSchemaSend( requestOptions: {}, }, ): StreamableMethod { + const path = expandUrlTemplate( + "/$schemaGroups/{groupName}/schemas/{name}{?api-version}", + { + groupName: groupName, + name: name, + }, + { + allowReserved: options?.requestOptions?.skipUrlEncoding, + }, + ); return context - .path("/$schemaGroups/{groupName}/schemas/{name}", groupName, name) + .path(path) .put({ ...operationOptionsToRequestParameters(options), contentType: contentType, diff --git a/packages/typespec-test/test/schemaRegistry/generated/typespec-ts/src/static-helpers/urlTemplate.ts b/packages/typespec-test/test/schemaRegistry/generated/typespec-ts/src/static-helpers/urlTemplate.ts new file mode 100644 index 0000000000..3d56a97a31 --- /dev/null +++ b/packages/typespec-test/test/schemaRegistry/generated/typespec-ts/src/static-helpers/urlTemplate.ts @@ -0,0 +1,199 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +//--------------------- +// interfaces +//--------------------- +interface ValueOptions { + isFirst: boolean; // is first value in the expression + op?: string; // operator + varValue?: any; // variable value + varName?: string; // variable name + modifier?: string; // modifier e.g * + reserved: boolean; // if true we'll keep reserved words with not encoding +} + +export interface UrlTemplateOptions { + // if set to true, reserved characters will not be encoded + allowReserved?: boolean; +} + +// --------------------- +// helpers +// --------------------- +function encodeValue(val: string, reserved: boolean, op?: string) { + return reserved === true || op === "+" || op === "#" + ? encodeWithReserved(val) + : encodeRFC3986URIComponent(val); +} + +function encodeWithReserved(str: string) { + return str + .split(/(%[0-9A-Fa-f]{2})/g) + .map((part) => (!/%[0-9A-Fa-f]/.test(part) ? encodeURI(part) : part)) + .join(""); +} + +function encodeRFC3986URIComponent(str: string) { + return encodeURIComponent(str).replace( + /[!'()*]/g, + (c) => `%${c.charCodeAt(0).toString(16).toUpperCase()}`, + ); +} + +function isDefined(val: any) { + return val !== undefined && val !== null; +} + +function getNamedAndIfEmpty(op?: string): [boolean, string] { + return [ + !!op && [";", "?", "&"].includes(op), + !!op && ["?", "&"].includes(op) ? "=" : "", + ]; +} + +function getFirstOrSep(op?: string, isFirst = false) { + if (isFirst) { + return !op || op === "+" ? "" : op; + } else if (!op || op === "+" || op === "#") { + return ","; + } else if (op === "?") { + return "&"; + } else { + return op; + } +} + +function getExpandedValue(option: ValueOptions) { + let isFirst = option.isFirst; + const { op, varName, varValue: value, reserved } = option; + const vals: string[] = []; + const [named, ifEmpty] = getNamedAndIfEmpty(op); + + if (Array.isArray(value)) { + for (const val of value.filter(isDefined)) { + // prepare the following parts: separator, varName, value + vals.push(`${getFirstOrSep(op, isFirst)}`); + if (named && varName) { + vals.push(`${encodeURIComponent(varName)}`); + val === "" ? vals.push(ifEmpty) : vals.push("="); + } + vals.push(encodeValue(val, reserved, op)); + isFirst = false; + } + } else if (typeof value === "object") { + for (const key of Object.keys(value)) { + const val = value[key]; + if (!isDefined(val)) { + continue; + } + // prepare the following parts: separator, key, value + vals.push(`${getFirstOrSep(op, isFirst)}`); + if (key) { + vals.push(`${encodeURIComponent(key)}`); + named && val === "" ? vals.push(ifEmpty) : vals.push("="); + } + vals.push(encodeValue(val, reserved, op)); + isFirst = false; + } + } + return vals.join(""); +} + +function getNonExpandedValue(option: ValueOptions) { + const { op, varName, varValue: value, isFirst, reserved } = option; + const vals: string[] = []; + const first = getFirstOrSep(op, isFirst); + const [named, ifEmpty] = getNamedAndIfEmpty(op); + if (named && varName) { + vals.push(encodeValue(varName, reserved, op)); + if (value === "") { + if (!ifEmpty) { + vals.push(ifEmpty); + } + return !vals.join("") ? undefined : `${first}${vals.join("")}`; + } + vals.push("="); + } + + const items = []; + if (Array.isArray(value)) { + for (const val of value.filter(isDefined)) { + items.push(encodeValue(val, reserved, op)); + } + } else if (typeof value === "object") { + for (const key of Object.keys(value)) { + if (!isDefined(value[key])) { + continue; + } + items.push(encodeRFC3986URIComponent(key)); + items.push(encodeValue(value[key], reserved, op)); + } + } + vals.push(items.join(",")); + return !vals.join(",") ? undefined : `${first}${vals.join("")}`; +} + +function getVarValue(option: ValueOptions): string | undefined { + const { op, varName, modifier, isFirst, reserved, varValue: value } = option; + + if (!isDefined(value)) { + return undefined; + } else if (["string", "number", "boolean"].includes(typeof value)) { + let val = value.toString(); + const [named, ifEmpty] = getNamedAndIfEmpty(op); + const vals: string[] = [getFirstOrSep(op, isFirst)]; + if (named && varName) { + vals.push(encodeRFC3986URIComponent(varName)); + val === "" ? vals.push(ifEmpty) : vals.push("="); + } + if (modifier && modifier !== "*") { + val = val.substring(0, parseInt(modifier, 10)); + } + vals.push(encodeValue(val, reserved, op)); + return vals.join(""); + } else if (modifier === "*") { + return getExpandedValue(option); + } else { + return getNonExpandedValue(option); + } +} + +// --------------------------------------------------------------------------------------------------- +// This is an implementation of RFC 6570 URI Template: https://datatracker.ietf.org/doc/html/rfc6570. +// --------------------------------------------------------------------------------------------------- +export function expandUrlTemplate( + template: string, + context: Record, + option?: UrlTemplateOptions, +): string { + return template.replace(/\{([^\{\}]+)\}|([^\{\}]+)/g, (_, expr, text) => { + if (!expr) { + return encodeWithReserved(text); + } + let op; + if (["+", "#", ".", "/", ";", "?", "&"].includes(expr[0])) { + (op = expr[0]), (expr = expr.slice(1)); + } + const varList = expr.split(/,/g); + const result = []; + for (const varSpec of varList) { + const varMatch = /([^:\*]*)(?::(\d+)|(\*))?/.exec(varSpec); + if (!varMatch || !varMatch[1]) { + continue; + } + const varValue = getVarValue({ + isFirst: result.length === 0, + op, + varValue: context[varMatch[1]], + varName: varMatch[1], + modifier: varMatch[2] || varMatch[3], + reserved: option?.allowReserved ?? false, + }); + if (varValue) { + result.push(varValue); + } + } + return result.join(""); + }); +} diff --git a/packages/typespec-test/test/todo_non_branded/generated/typespec-ts/src/api/todoItems/attachments/index.ts b/packages/typespec-test/test/todo_non_branded/generated/typespec-ts/src/api/todoItems/attachments/index.ts index de6f596f99..cfda3f0536 100644 --- a/packages/typespec-test/test/todo_non_branded/generated/typespec-ts/src/api/todoItems/attachments/index.ts +++ b/packages/typespec-test/test/todo_non_branded/generated/typespec-ts/src/api/todoItems/attachments/index.ts @@ -11,6 +11,7 @@ import { PageTodoAttachment, pageTodoAttachmentDeserializer, } from "../../../models/models.js"; +import { expandUrlTemplate } from "../../../static-helpers/urlTemplate.js"; import { StreamableMethod, PathUncheckedResponse, @@ -23,8 +24,17 @@ export function _listSend( itemId: number, options: TodoItemsAttachmentsListOptionalParams = { requestOptions: {} }, ): StreamableMethod { + const path = expandUrlTemplate( + "/items/{itemId}/attachments", + { + itemId: itemId, + }, + { + allowReserved: options?.requestOptions?.skipUrlEncoding, + }, + ); return context - .path("/items/{itemId}/attachments", itemId) + .path(path) .get({ ...operationOptionsToRequestParameters(options) }); } @@ -56,8 +66,17 @@ export function _createAttachmentSend( requestOptions: {}, }, ): StreamableMethod { + const path = expandUrlTemplate( + "/items/{itemId}/attachments", + { + itemId: itemId, + }, + { + allowReserved: options?.requestOptions?.skipUrlEncoding, + }, + ); return context - .path("/items/{itemId}/attachments", itemId) + .path(path) .post({ ...operationOptionsToRequestParameters(options), body: todoAttachmentSerializer(contents), diff --git a/packages/typespec-test/test/todo_non_branded/generated/typespec-ts/src/api/todoItems/index.ts b/packages/typespec-test/test/todo_non_branded/generated/typespec-ts/src/api/todoItems/index.ts index 0ec78284b8..bd50c4f603 100644 --- a/packages/typespec-test/test/todo_non_branded/generated/typespec-ts/src/api/todoItems/index.ts +++ b/packages/typespec-test/test/todo_non_branded/generated/typespec-ts/src/api/todoItems/index.ts @@ -21,6 +21,7 @@ import { todoItemPatchSerializer, _updateResponseDeserializer, } from "../../models/models.js"; +import { expandUrlTemplate } from "../../static-helpers/urlTemplate.js"; import { StreamableMethod, PathUncheckedResponse, @@ -32,12 +33,19 @@ export function _listSend( context: Client, options: TodoItemsListOptionalParams = { requestOptions: {} }, ): StreamableMethod { + const path = expandUrlTemplate( + "/items{?limit,offset}", + { + limit: options?.limit, + offset: options?.offset, + }, + { + allowReserved: options?.requestOptions?.skipUrlEncoding, + }, + ); return context - .path("/items") - .get({ - ...operationOptionsToRequestParameters(options), - queryParameters: { limit: options?.limit, offset: options?.offset }, - }); + .path(path) + .get({ ...operationOptionsToRequestParameters(options) }); } export async function _listDeserialize( @@ -134,8 +142,17 @@ export function _getSend( id: number, options: TodoItemsGetOptionalParams = { requestOptions: {} }, ): StreamableMethod { + const path = expandUrlTemplate( + "/items/{id}", + { + id: id, + }, + { + allowReserved: options?.requestOptions?.skipUrlEncoding, + }, + ); return context - .path("/items/{id}", id) + .path(path) .get({ ...operationOptionsToRequestParameters(options) }); } @@ -185,8 +202,17 @@ export function _updateSend( patch: TodoItemPatch, options: TodoItemsUpdateOptionalParams = { requestOptions: {} }, ): StreamableMethod { + const path = expandUrlTemplate( + "/items/{id}", + { + id: id, + }, + { + allowReserved: options?.requestOptions?.skipUrlEncoding, + }, + ); return context - .path("/items/{id}", id) + .path(path) .patch({ ...operationOptionsToRequestParameters(options), contentType: @@ -243,8 +269,17 @@ export function _$deleteSend( id: number, options: TodoItemsDeleteOptionalParams = { requestOptions: {} }, ): StreamableMethod { + const path = expandUrlTemplate( + "/items/{id}", + { + id: id, + }, + { + allowReserved: options?.requestOptions?.skipUrlEncoding, + }, + ); return context - .path("/items/{id}", id) + .path(path) .delete({ ...operationOptionsToRequestParameters(options) }); } diff --git a/packages/typespec-test/test/todo_non_branded/generated/typespec-ts/src/static-helpers/urlTemplate.ts b/packages/typespec-test/test/todo_non_branded/generated/typespec-ts/src/static-helpers/urlTemplate.ts new file mode 100644 index 0000000000..c768c23002 --- /dev/null +++ b/packages/typespec-test/test/todo_non_branded/generated/typespec-ts/src/static-helpers/urlTemplate.ts @@ -0,0 +1,198 @@ +// Licensed under the MIT License. + +//--------------------- +// interfaces +//--------------------- +interface ValueOptions { + isFirst: boolean; // is first value in the expression + op?: string; // operator + varValue?: any; // variable value + varName?: string; // variable name + modifier?: string; // modifier e.g * + reserved: boolean; // if true we'll keep reserved words with not encoding +} + +export interface UrlTemplateOptions { + // if set to true, reserved characters will not be encoded + allowReserved?: boolean; +} + +// --------------------- +// helpers +// --------------------- +function encodeValue(val: string, reserved: boolean, op?: string) { + return reserved === true || op === "+" || op === "#" + ? encodeWithReserved(val) + : encodeRFC3986URIComponent(val); +} + +function encodeWithReserved(str: string) { + return str + .split(/(%[0-9A-Fa-f]{2})/g) + .map((part) => (!/%[0-9A-Fa-f]/.test(part) ? encodeURI(part) : part)) + .join(""); +} + +function encodeRFC3986URIComponent(str: string) { + return encodeURIComponent(str).replace( + /[!'()*]/g, + (c) => `%${c.charCodeAt(0).toString(16).toUpperCase()}`, + ); +} + +function isDefined(val: any) { + return val !== undefined && val !== null; +} + +function getNamedAndIfEmpty(op?: string): [boolean, string] { + return [ + !!op && [";", "?", "&"].includes(op), + !!op && ["?", "&"].includes(op) ? "=" : "", + ]; +} + +function getFirstOrSep(op?: string, isFirst = false) { + if (isFirst) { + return !op || op === "+" ? "" : op; + } else if (!op || op === "+" || op === "#") { + return ","; + } else if (op === "?") { + return "&"; + } else { + return op; + } +} + +function getExpandedValue(option: ValueOptions) { + let isFirst = option.isFirst; + const { op, varName, varValue: value, reserved } = option; + const vals: string[] = []; + const [named, ifEmpty] = getNamedAndIfEmpty(op); + + if (Array.isArray(value)) { + for (const val of value.filter(isDefined)) { + // prepare the following parts: separator, varName, value + vals.push(`${getFirstOrSep(op, isFirst)}`); + if (named && varName) { + vals.push(`${encodeURIComponent(varName)}`); + val === "" ? vals.push(ifEmpty) : vals.push("="); + } + vals.push(encodeValue(val, reserved, op)); + isFirst = false; + } + } else if (typeof value === "object") { + for (const key of Object.keys(value)) { + const val = value[key]; + if (!isDefined(val)) { + continue; + } + // prepare the following parts: separator, key, value + vals.push(`${getFirstOrSep(op, isFirst)}`); + if (key) { + vals.push(`${encodeURIComponent(key)}`); + named && val === "" ? vals.push(ifEmpty) : vals.push("="); + } + vals.push(encodeValue(val, reserved, op)); + isFirst = false; + } + } + return vals.join(""); +} + +function getNonExpandedValue(option: ValueOptions) { + const { op, varName, varValue: value, isFirst, reserved } = option; + const vals: string[] = []; + const first = getFirstOrSep(op, isFirst); + const [named, ifEmpty] = getNamedAndIfEmpty(op); + if (named && varName) { + vals.push(encodeValue(varName, reserved, op)); + if (value === "") { + if (!ifEmpty) { + vals.push(ifEmpty); + } + return !vals.join("") ? undefined : `${first}${vals.join("")}`; + } + vals.push("="); + } + + const items = []; + if (Array.isArray(value)) { + for (const val of value.filter(isDefined)) { + items.push(encodeValue(val, reserved, op)); + } + } else if (typeof value === "object") { + for (const key of Object.keys(value)) { + if (!isDefined(value[key])) { + continue; + } + items.push(encodeRFC3986URIComponent(key)); + items.push(encodeValue(value[key], reserved, op)); + } + } + vals.push(items.join(",")); + return !vals.join(",") ? undefined : `${first}${vals.join("")}`; +} + +function getVarValue(option: ValueOptions): string | undefined { + const { op, varName, modifier, isFirst, reserved, varValue: value } = option; + + if (!isDefined(value)) { + return undefined; + } else if (["string", "number", "boolean"].includes(typeof value)) { + let val = value.toString(); + const [named, ifEmpty] = getNamedAndIfEmpty(op); + const vals: string[] = [getFirstOrSep(op, isFirst)]; + if (named && varName) { + vals.push(encodeRFC3986URIComponent(varName)); + val === "" ? vals.push(ifEmpty) : vals.push("="); + } + if (modifier && modifier !== "*") { + val = val.substring(0, parseInt(modifier, 10)); + } + vals.push(encodeValue(val, reserved, op)); + return vals.join(""); + } else if (modifier === "*") { + return getExpandedValue(option); + } else { + return getNonExpandedValue(option); + } +} + +// --------------------------------------------------------------------------------------------------- +// This is an implementation of RFC 6570 URI Template: https://datatracker.ietf.org/doc/html/rfc6570. +// --------------------------------------------------------------------------------------------------- +export function expandUrlTemplate( + template: string, + context: Record, + option?: UrlTemplateOptions, +): string { + return template.replace(/\{([^\{\}]+)\}|([^\{\}]+)/g, (_, expr, text) => { + if (!expr) { + return encodeWithReserved(text); + } + let op; + if (["+", "#", ".", "/", ";", "?", "&"].includes(expr[0])) { + (op = expr[0]), (expr = expr.slice(1)); + } + const varList = expr.split(/,/g); + const result = []; + for (const varSpec of varList) { + const varMatch = /([^:\*]*)(?::(\d+)|(\*))?/.exec(varSpec); + if (!varMatch || !varMatch[1]) { + continue; + } + const varValue = getVarValue({ + isFirst: result.length === 0, + op, + varValue: context[varMatch[1]], + varName: varMatch[1], + modifier: varMatch[2] || varMatch[3], + reserved: option?.allowReserved ?? false, + }); + if (varValue) { + result.push(varValue); + } + } + return result.join(""); + }); +} diff --git a/packages/typespec-test/test/widget_dpg/generated/typespec-ts/src/api/budgets/index.ts b/packages/typespec-test/test/widget_dpg/generated/typespec-ts/src/api/budgets/index.ts index ea721ea5b0..f5bbab3e32 100644 --- a/packages/typespec-test/test/widget_dpg/generated/typespec-ts/src/api/budgets/index.ts +++ b/packages/typespec-test/test/widget_dpg/generated/typespec-ts/src/api/budgets/index.ts @@ -7,6 +7,7 @@ import { } from "../index.js"; import { User, userSerializer, userDeserializer } from "../../models/models.js"; import { getLongRunningPoller } from "../../static-helpers/pollingHelpers.js"; +import { expandUrlTemplate } from "../../static-helpers/urlTemplate.js"; import { StreamableMethod, PathUncheckedResponse, @@ -21,11 +22,20 @@ export function _createOrReplaceSend( resource: User, options: BudgetsCreateOrReplaceOptionalParams = { requestOptions: {} }, ): StreamableMethod { + const path = expandUrlTemplate( + "/budgets/widgets/createOrReplace/users/{name}{?api-version}", + { + name: name, + "api-version": options?.apiVersion ?? "1.0.0", + }, + { + allowReserved: options?.requestOptions?.skipUrlEncoding, + }, + ); return context - .path("/budgets/widgets/createOrReplace/users/{name}", name) + .path(path) .put({ ...operationOptionsToRequestParameters(options), - queryParameters: { "api-version": options?.apiVersion ?? "1.0.0" }, body: userSerializer(resource), }); } diff --git a/packages/typespec-test/test/widget_dpg/generated/typespec-ts/src/api/widgets/index.ts b/packages/typespec-test/test/widget_dpg/generated/typespec-ts/src/api/widgets/index.ts index 6d91eb21c7..c470f4bcb1 100644 --- a/packages/typespec-test/test/widget_dpg/generated/typespec-ts/src/api/widgets/index.ts +++ b/packages/typespec-test/test/widget_dpg/generated/typespec-ts/src/api/widgets/index.ts @@ -30,6 +30,7 @@ import { buildPagedAsyncIterator, } from "../../static-helpers/pagingHelpers.js"; import { getLongRunningPoller } from "../../static-helpers/pollingHelpers.js"; +import { expandUrlTemplate } from "../../static-helpers/urlTemplate.js"; import { buildCsvCollection } from "../../static-helpers/serialization/build-csv-collection.js"; import { StreamableMethod, @@ -130,12 +131,19 @@ export function _listWidgetsPagesSend( pageSize: number, options: WidgetsListWidgetsPagesOptionalParams = { requestOptions: {} }, ): StreamableMethod { + const path = expandUrlTemplate( + "/widgets/widgets/pages{?page,pageSize}", + { + page: page, + pageSize: pageSize, + }, + { + allowReserved: options?.requestOptions?.skipUrlEncoding, + }, + ); return context - .path("/widgets/widgets/pages") - .get({ - ...operationOptionsToRequestParameters(options), - queryParameters: { page: page, pageSize: pageSize }, - }); + .path(path) + .get({ ...operationOptionsToRequestParameters(options) }); } export async function _listWidgetsPagesDeserialize( @@ -170,12 +178,19 @@ export function _queryWidgetsPagesSend( pageSize: number, options: WidgetsQueryWidgetsPagesOptionalParams = { requestOptions: {} }, ): StreamableMethod { + const path = expandUrlTemplate( + "/widgets/widgets/pages{?page,pageSize}", + { + page: page, + pageSize: pageSize, + }, + { + allowReserved: options?.requestOptions?.skipUrlEncoding, + }, + ); return context - .path("/widgets/widgets/pages") - .post({ - ...operationOptionsToRequestParameters(options), - queryParameters: { page: page, pageSize: pageSize }, - }); + .path(path) + .post({ ...operationOptionsToRequestParameters(options) }); } export async function _queryWidgetsPagesDeserialize( @@ -209,8 +224,17 @@ export function _getWidgetSend( id: string, options: WidgetsGetWidgetOptionalParams = { requestOptions: {} }, ): StreamableMethod { + const path = expandUrlTemplate( + "/widgets/{id}", + { + id: id, + }, + { + allowReserved: options?.requestOptions?.skipUrlEncoding, + }, + ); return context - .path("/widgets/{id}", id) + .path(path) .get({ ...operationOptionsToRequestParameters(options) }); } @@ -282,11 +306,20 @@ export function _createOrReplaceSend( resource: User, options: WidgetsCreateOrReplaceOptionalParams = { requestOptions: {} }, ): StreamableMethod { + const path = expandUrlTemplate( + "/widgets/widgets/createOrReplace/users/{name}{?api-version}", + { + name: name, + "api-version": options?.apiVersion ?? "1.0.0", + }, + { + allowReserved: options?.requestOptions?.skipUrlEncoding, + }, + ); return context - .path("/widgets/widgets/createOrReplace/users/{name}", name) + .path(path) .put({ ...operationOptionsToRequestParameters(options), - queryParameters: { "api-version": options?.apiVersion ?? "1.0.0" }, body: userSerializer(resource), }); } @@ -328,8 +361,17 @@ export function _updateWidgetSend( id: string, options: WidgetsUpdateWidgetOptionalParams = { requestOptions: {} }, ): StreamableMethod { + const path = expandUrlTemplate( + "/widgets/{id}", + { + id: id, + }, + { + allowReserved: options?.requestOptions?.skipUrlEncoding, + }, + ); return context - .path("/widgets/{id}", id) + .path(path) .patch({ ...operationOptionsToRequestParameters(options), body: { weight: options?.weight, color: options?.color }, @@ -365,8 +407,17 @@ export function _deleteWidgetSend( id: string, options: WidgetsDeleteWidgetOptionalParams = { requestOptions: {} }, ): StreamableMethod { + const path = expandUrlTemplate( + "/widgets/{id}", + { + id: id, + }, + { + allowReserved: options?.requestOptions?.skipUrlEncoding, + }, + ); return context - .path("/widgets/{id}", id) + .path(path) .delete({ ...operationOptionsToRequestParameters(options) }); } @@ -396,8 +447,17 @@ export function _analyzeWidgetSend( id: string, options: WidgetsAnalyzeWidgetOptionalParams = { requestOptions: {} }, ): StreamableMethod { + const path = expandUrlTemplate( + "/widgets/{id}/analyze", + { + id: id, + }, + { + allowReserved: options?.requestOptions?.skipUrlEncoding, + }, + ); return context - .path("/widgets/{id}/analyze", id) + .path(path) .post({ ...operationOptionsToRequestParameters(options) }); } diff --git a/packages/typespec-test/test/widget_dpg/generated/typespec-ts/src/static-helpers/urlTemplate.ts b/packages/typespec-test/test/widget_dpg/generated/typespec-ts/src/static-helpers/urlTemplate.ts new file mode 100644 index 0000000000..3d56a97a31 --- /dev/null +++ b/packages/typespec-test/test/widget_dpg/generated/typespec-ts/src/static-helpers/urlTemplate.ts @@ -0,0 +1,199 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +//--------------------- +// interfaces +//--------------------- +interface ValueOptions { + isFirst: boolean; // is first value in the expression + op?: string; // operator + varValue?: any; // variable value + varName?: string; // variable name + modifier?: string; // modifier e.g * + reserved: boolean; // if true we'll keep reserved words with not encoding +} + +export interface UrlTemplateOptions { + // if set to true, reserved characters will not be encoded + allowReserved?: boolean; +} + +// --------------------- +// helpers +// --------------------- +function encodeValue(val: string, reserved: boolean, op?: string) { + return reserved === true || op === "+" || op === "#" + ? encodeWithReserved(val) + : encodeRFC3986URIComponent(val); +} + +function encodeWithReserved(str: string) { + return str + .split(/(%[0-9A-Fa-f]{2})/g) + .map((part) => (!/%[0-9A-Fa-f]/.test(part) ? encodeURI(part) : part)) + .join(""); +} + +function encodeRFC3986URIComponent(str: string) { + return encodeURIComponent(str).replace( + /[!'()*]/g, + (c) => `%${c.charCodeAt(0).toString(16).toUpperCase()}`, + ); +} + +function isDefined(val: any) { + return val !== undefined && val !== null; +} + +function getNamedAndIfEmpty(op?: string): [boolean, string] { + return [ + !!op && [";", "?", "&"].includes(op), + !!op && ["?", "&"].includes(op) ? "=" : "", + ]; +} + +function getFirstOrSep(op?: string, isFirst = false) { + if (isFirst) { + return !op || op === "+" ? "" : op; + } else if (!op || op === "+" || op === "#") { + return ","; + } else if (op === "?") { + return "&"; + } else { + return op; + } +} + +function getExpandedValue(option: ValueOptions) { + let isFirst = option.isFirst; + const { op, varName, varValue: value, reserved } = option; + const vals: string[] = []; + const [named, ifEmpty] = getNamedAndIfEmpty(op); + + if (Array.isArray(value)) { + for (const val of value.filter(isDefined)) { + // prepare the following parts: separator, varName, value + vals.push(`${getFirstOrSep(op, isFirst)}`); + if (named && varName) { + vals.push(`${encodeURIComponent(varName)}`); + val === "" ? vals.push(ifEmpty) : vals.push("="); + } + vals.push(encodeValue(val, reserved, op)); + isFirst = false; + } + } else if (typeof value === "object") { + for (const key of Object.keys(value)) { + const val = value[key]; + if (!isDefined(val)) { + continue; + } + // prepare the following parts: separator, key, value + vals.push(`${getFirstOrSep(op, isFirst)}`); + if (key) { + vals.push(`${encodeURIComponent(key)}`); + named && val === "" ? vals.push(ifEmpty) : vals.push("="); + } + vals.push(encodeValue(val, reserved, op)); + isFirst = false; + } + } + return vals.join(""); +} + +function getNonExpandedValue(option: ValueOptions) { + const { op, varName, varValue: value, isFirst, reserved } = option; + const vals: string[] = []; + const first = getFirstOrSep(op, isFirst); + const [named, ifEmpty] = getNamedAndIfEmpty(op); + if (named && varName) { + vals.push(encodeValue(varName, reserved, op)); + if (value === "") { + if (!ifEmpty) { + vals.push(ifEmpty); + } + return !vals.join("") ? undefined : `${first}${vals.join("")}`; + } + vals.push("="); + } + + const items = []; + if (Array.isArray(value)) { + for (const val of value.filter(isDefined)) { + items.push(encodeValue(val, reserved, op)); + } + } else if (typeof value === "object") { + for (const key of Object.keys(value)) { + if (!isDefined(value[key])) { + continue; + } + items.push(encodeRFC3986URIComponent(key)); + items.push(encodeValue(value[key], reserved, op)); + } + } + vals.push(items.join(",")); + return !vals.join(",") ? undefined : `${first}${vals.join("")}`; +} + +function getVarValue(option: ValueOptions): string | undefined { + const { op, varName, modifier, isFirst, reserved, varValue: value } = option; + + if (!isDefined(value)) { + return undefined; + } else if (["string", "number", "boolean"].includes(typeof value)) { + let val = value.toString(); + const [named, ifEmpty] = getNamedAndIfEmpty(op); + const vals: string[] = [getFirstOrSep(op, isFirst)]; + if (named && varName) { + vals.push(encodeRFC3986URIComponent(varName)); + val === "" ? vals.push(ifEmpty) : vals.push("="); + } + if (modifier && modifier !== "*") { + val = val.substring(0, parseInt(modifier, 10)); + } + vals.push(encodeValue(val, reserved, op)); + return vals.join(""); + } else if (modifier === "*") { + return getExpandedValue(option); + } else { + return getNonExpandedValue(option); + } +} + +// --------------------------------------------------------------------------------------------------- +// This is an implementation of RFC 6570 URI Template: https://datatracker.ietf.org/doc/html/rfc6570. +// --------------------------------------------------------------------------------------------------- +export function expandUrlTemplate( + template: string, + context: Record, + option?: UrlTemplateOptions, +): string { + return template.replace(/\{([^\{\}]+)\}|([^\{\}]+)/g, (_, expr, text) => { + if (!expr) { + return encodeWithReserved(text); + } + let op; + if (["+", "#", ".", "/", ";", "?", "&"].includes(expr[0])) { + (op = expr[0]), (expr = expr.slice(1)); + } + const varList = expr.split(/,/g); + const result = []; + for (const varSpec of varList) { + const varMatch = /([^:\*]*)(?::(\d+)|(\*))?/.exec(varSpec); + if (!varMatch || !varMatch[1]) { + continue; + } + const varValue = getVarValue({ + isFirst: result.length === 0, + op, + varValue: context[varMatch[1]], + varName: varMatch[1], + modifier: varMatch[2] || varMatch[3], + reserved: option?.allowReserved ?? false, + }); + if (varValue) { + result.push(varValue); + } + } + return result.join(""); + }); +} diff --git a/packages/typespec-ts/package.json b/packages/typespec-ts/package.json index f1828c9d21..501329ba5c 100644 --- a/packages/typespec-ts/package.json +++ b/packages/typespec-ts/package.json @@ -57,7 +57,7 @@ "stop-test-server": "npx cadl-ranch server stop", "unit-test": "npm-run-all --parallel unit-test:rlc unit-test:modular", "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'", - "unit-test:modular": "cross-env TS_NODE_PROJECT=tsconfig.json mocha -r ts-node/register --experimental-specifier-resolution=node --experimental-modules=true --no-timeout './test/modularUnit/**/*.spec.ts'" + "unit-test:modular": "cross-env TS_NODE_PROJECT=tsconfig.test.json mocha -r ts-node/register --experimental-specifier-resolution=node --experimental-modules=true --no-timeout './test/modularUnit/**/*.spec.ts'" }, "author": "Jose Heredia ", "license": "MIT", diff --git a/packages/typespec-ts/src/index.ts b/packages/typespec-ts/src/index.ts index 197e12920e..6b130cbefb 100644 --- a/packages/typespec-ts/src/index.ts +++ b/packages/typespec-ts/src/index.ts @@ -13,7 +13,8 @@ import { GenerationDirDetail, SdkContext } from "./utils/interfaces.js"; import { PagingHelpers, PollingHelpers, - SerializationHelpers + SerializationHelpers, + UrlTemplateHelpers } from "./modular/static-helpers-metadata.js"; import { RLCModel, @@ -116,7 +117,8 @@ export async function $onEmit(context: EmitContext) { { ...SerializationHelpers, ...PagingHelpers, - ...PollingHelpers + ...PollingHelpers, + ...UrlTemplateHelpers }, { sourcesDir: dpgContext.generationPathDetail?.modularSourcesDir } ); diff --git a/packages/typespec-ts/src/modular/buildCodeModel.ts b/packages/typespec-ts/src/modular/buildCodeModel.ts index 9f0c8e24c6..58f6676f01 100644 --- a/packages/typespec-ts/src/modular/buildCodeModel.ts +++ b/packages/typespec-ts/src/modular/buildCodeModel.ts @@ -971,6 +971,7 @@ function emitBasicOperation( description: getDocStr(context.program, operation), summary: getSummary(context.program, operation) ?? "", url: httpOperation.path, + urlTemplate: httpOperation.uriTemplate, method: httpOperation.verb.toUpperCase(), parameters: parameters, bodyParameter: bodyParameter, diff --git a/packages/typespec-ts/src/modular/helpers/classicalOperationHelpers.ts b/packages/typespec-ts/src/modular/helpers/classicalOperationHelpers.ts index facf2313b1..94c8d49f66 100644 --- a/packages/typespec-ts/src/modular/helpers/classicalOperationHelpers.ts +++ b/packages/typespec-ts/src/modular/helpers/classicalOperationHelpers.ts @@ -124,7 +124,11 @@ export function getClassicalOperation( }); } if (existInterface) { - existInterface.addProperties([...properties]); + // only adding the property if it doesn't exist + const unExistingProperties = properties.filter( + (p) => !existInterface.getProperty(p.name) + ); + existInterface.addProperties([...unExistingProperties]); } else { classicFile.addInterface({ name: interfaceName, @@ -205,8 +209,7 @@ export function getClassicalOperation( hasSubscriptionIdPromoted ? ", subscriptionId" : "" })}`; if (layer !== operationGroup.namespaceHierarchies.length - 1) { - statement = `, - ${normalizeName( + const propertyAndGetterStatement = `${normalizeName( operationGroup.namespaceHierarchies[layer + 1] ?? "FIXME", NameType.Property )}: get${getClassicalLayerPrefix( @@ -214,9 +217,16 @@ export function getClassicalOperation( NameType.Interface, "", layer + 1 - )}Operations(context${ + )}`; + // only adding the property if it doesn't exist + if (!returnStatement.includes(propertyAndGetterStatement)) { + statement = `, + ${propertyAndGetterStatement}Operations(context${ hasSubscriptionIdPromoted ? ", subscriptionId" : "" })}`; + } else { + statement = `}`; + } } const newReturnStatement = returnStatement.replace(/}$/, statement); existFunction.setBodyText(newReturnStatement); diff --git a/packages/typespec-ts/src/modular/helpers/operationHelpers.ts b/packages/typespec-ts/src/modular/helpers/operationHelpers.ts index 51f4dca51c..f14e27f736 100644 --- a/packages/typespec-ts/src/modular/helpers/operationHelpers.ts +++ b/packages/typespec-ts/src/modular/helpers/operationHelpers.ts @@ -13,7 +13,11 @@ import { ParameterDeclarationStructure } from "ts-morph"; import { NoTarget, Program } from "@typespec/compiler"; -import { PagingHelpers, PollingHelpers } from "../static-helpers-metadata.js"; +import { + PagingHelpers, + PollingHelpers, + UrlTemplateHelpers +} from "../static-helpers-metadata.js"; import { getType, isTypeNullable } from "./typeHelpers.js"; import { getClassicalLayerPrefix, getOperationName } from "./namingHelpers.js"; import { @@ -63,19 +67,27 @@ export function getSendPrivateFunction( parameters, returnType: resolveReference(dependencies.StreamableMethod) }; - - const operationPath = operation.url; const operationMethod = operation.method.toLowerCase(); const optionalParamName = parameters.filter((p) => p.type?.toString().endsWith("OptionalParams") )[0]?.name; - const statements: string[] = []; + let pathStr = `"${operation.url}"`; + const urlTemplateParams = [ + ...getPathParameters(dpgContext, operation), + ...getQueryParameters(dpgContext, operation) + ]; + if (urlTemplateParams.length > 0) { + statements.push(`const path = ${resolveReference(UrlTemplateHelpers.parseTemplate)}("${operation.urlTemplate}", { + ${urlTemplateParams.join(",\n")} + },{ + allowReserved: ${optionalParamName}?.requestOptions?.skipUrlEncoding + });`); + pathStr = "path"; + } + statements.push( - `return context.path("${operationPath}", ${getPathParameters( - dpgContext, - operation - )}).${operationMethod}({...${resolveReference(dependencies.operationOptionsToRequestParameters)}(${optionalParamName}), ${getRequestParameters( + `return context.path(${pathStr}).${operationMethod}({...${resolveReference(dependencies.operationOptionsToRequestParameters)}(${optionalParamName}), ${getHeaderAndBodyParameters( dpgContext, operation )}});` @@ -488,7 +500,7 @@ export function getOperationOptionsName( * RLC internally. This will translate High Level parameters into the RLC ones. * Figuring out what goes in headers, body, path and qsp. */ -function getRequestParameters( +function getHeaderAndBodyParameters( dpgContext: SdkContext, operation: Operation ): string { @@ -502,20 +514,15 @@ function getRequestParameters( const contentTypeParameter = operation.parameters.find(isContentType); const parametersImplementation: Record< - "header" | "query" | "body", + "header" | "body", { paramMap: string; param: Parameter }[] > = { header: [], - query: [], body: [] }; for (const param of operationParameters) { - if ( - param.location === "header" || - param.location === "query" || - param.location === "body" - ) { + if (param.location === "header" || param.location === "body") { parametersImplementation[param.location].push({ paramMap: getParameterMap(dpgContext, param), param @@ -534,12 +541,6 @@ function getRequestParameters( .map((i) => buildHeaderParameter(dpgContext.program, i.paramMap, i.param)) .join(",\n")}},`; } - - if (parametersImplementation.query.length) { - paramStr = `${paramStr}\nqueryParameters: {${parametersImplementation.query - .map((i) => i.paramMap) - .join(",\n")}},`; - } if ( operation.bodyParameter === undefined && parametersImplementation.body.length @@ -553,6 +554,42 @@ function getRequestParameters( return paramStr; } +/** + * Extract the query parameters + */ +function getQueryParameters( + dpgContext: SdkContext, + operation: Operation +): string[] { + if (!operation.parameters) { + return []; + } + const operationParameters = operation.parameters.filter( + (p) => p.implementation !== "Client" && !isContentType(p) + ); + const parametersImplementation: Record< + "query", + { paramMap: string; param: Parameter }[] + > = { + query: [] + }; + + for (const param of operationParameters) { + if (param.location === "query") { + parametersImplementation[param.location].push({ + paramMap: getParameterMap(dpgContext, param), + param + }); + } + } + + const paramStr: string[] = parametersImplementation.query.map( + (i) => i.paramMap + ); + + return paramStr; +} + // Specially handle the type for headers because we only allow string/number/boolean values function buildHeaderParameter( program: Program, @@ -812,10 +849,10 @@ function getDefaultValue(param: Parameter | Property) { */ function getPathParameters(dpgContext: SdkContext, operation: Operation) { if (!operation.parameters) { - return ""; + return []; } - let pathParams = ""; + const pathParams: string[] = []; for (const param of operation.parameters) { if (param.location === "path") { // Path parameters cannot be optional @@ -828,10 +865,9 @@ function getPathParameters(dpgContext: SdkContext, operation: Operation) { } }); } - pathParams += `${pathParams !== "" ? "," : ""} ${getPathParamExpr( - param, - getDefaultValue(param) - )}`; + pathParams.push( + `${param.clientName}: ${getPathParamExpr(param, getDefaultValue(param))}` + ); } } @@ -839,15 +875,11 @@ function getPathParameters(dpgContext: SdkContext, operation: Operation) { } function getPathParamExpr(param: Parameter, defaultValue?: string) { - const value = defaultValue + return defaultValue ? typeof defaultValue === "string" ? `options[${param.clientName}] ?? "${defaultValue}"` : `options[${param.clientName}] ?? ${defaultValue}` : param.clientName; - if (param.skipUrlEncoding === true) { - return `{value: ${value}, allowReserved: true}`; - } - return value; } function getNullableCheck(name: string, type: Type) { diff --git a/packages/typespec-ts/src/modular/modularCodeModel.ts b/packages/typespec-ts/src/modular/modularCodeModel.ts index d06653dbc9..fe8b051e09 100644 --- a/packages/typespec-ts/src/modular/modularCodeModel.ts +++ b/packages/typespec-ts/src/modular/modularCodeModel.ts @@ -185,6 +185,7 @@ export interface Operation { description: string; summary: string; url: string; + urlTemplate: string; method: string; parameters: Parameter[]; bodyParameter?: BodyParameter; diff --git a/packages/typespec-ts/src/modular/static-helpers-metadata.ts b/packages/typespec-ts/src/modular/static-helpers-metadata.ts index 360cd35e80..3ae50e5e28 100644 --- a/packages/typespec-ts/src/modular/static-helpers-metadata.ts +++ b/packages/typespec-ts/src/modular/static-helpers-metadata.ts @@ -71,3 +71,11 @@ export const PollingHelpers = { location: "pollingHelpers.ts" } } as const; + +export const UrlTemplateHelpers = { + parseTemplate: { + kind: "function", + name: "expandUrlTemplate", + location: "urlTemplate.ts" + } +} as const; diff --git a/packages/typespec-ts/src/utils/operationUtil.ts b/packages/typespec-ts/src/utils/operationUtil.ts index 3a6217ee34..3c04a47c74 100644 --- a/packages/typespec-ts/src/utils/operationUtil.ts +++ b/packages/typespec-ts/src/utils/operationUtil.ts @@ -470,7 +470,7 @@ export function hasCollectionFormatInfo( paramFormat: string ) { return ( - getHasMultiCollection(paramType, paramFormat) || + getHasMultiCollection(paramType, paramFormat, false) || getHasSsvCollection(paramType, paramFormat) || getHasTsvCollection(paramType, paramFormat) || getHasCsvCollection(paramType, paramFormat) || @@ -482,9 +482,7 @@ export function getCollectionFormatHelper( paramType: string, paramFormat: string ) { - // const detail = getSpecialSerializeInfo(paramType, paramFormat); - // return detail.descriptions.length > 0 ? detail.descriptions[0] : undefined; - if (getHasMultiCollection(paramType, paramFormat)) { + if (getHasMultiCollection(paramType, paramFormat, false)) { return resolveReference(SerializationHelpers.buildMultiCollection); } diff --git a/packages/typespec-ts/static/static-helpers/urlTemplate.ts b/packages/typespec-ts/static/static-helpers/urlTemplate.ts new file mode 100644 index 0000000000..905764a78d --- /dev/null +++ b/packages/typespec-ts/static/static-helpers/urlTemplate.ts @@ -0,0 +1,195 @@ +//--------------------- +// interfaces +//--------------------- +interface ValueOptions { + isFirst: boolean; // is first value in the expression + op?: string; // operator + varValue?: any; // variable value + varName?: string; // variable name + modifier?: string; // modifier e.g * + reserved: boolean; // if true we'll keep reserved words with not encoding +} + +export interface UrlTemplateOptions { + // if set to true, reserved characters will not be encoded + allowReserved?: boolean; +} + +// --------------------- +// helpers +// --------------------- +function encodeValue(val: string, reserved: boolean, op?: string) { + return reserved === true || op === "+" || op === "#" + ? encodeWithReserved(val) + : encodeRFC3986URIComponent(val); +} + +function encodeWithReserved(str: string) { + return str.split(/(%[0-9A-Fa-f]{2})/g) + .map(part => !/%[0-9A-Fa-f]/.test(part) ? encodeURI(part) : part) + .join(""); +} + +function encodeRFC3986URIComponent(str: string) { + return encodeURIComponent(str).replace( + /[!'()*]/g, + (c) => `%${c.charCodeAt(0).toString(16).toUpperCase()}`, + ); +} + +function isDefined(val: any) { + return val !== undefined && val !== null; +} + +function getNamedAndIfEmpty(op?: string): [boolean, string] { + return [ + !!op && [";", "?", "&"].includes(op), + !!op && ["?", "&"].includes(op) ? "=" : "", + ]; +} + +function getFirstOrSep(op?: string, isFirst = false) { + if (isFirst) { + return !op || op === "+" ? "" : op; + } else if (!op || op === "+" || op === "#") { + return ","; + } else if (op === "?") { + return "&"; + } else { + return op; + } +} + +function getExpandedValue(option: ValueOptions) { + let isFirst = option.isFirst; + const { op, varName, varValue: value, reserved } = option; + const vals: string[] = []; + const [named, ifEmpty] = getNamedAndIfEmpty(op); + + if (Array.isArray(value)) { + for (const val of value.filter(isDefined)) { + // prepare the following parts: separator, varName, value + vals.push(`${getFirstOrSep(op, isFirst)}`); + if (named && varName) { + vals.push(`${encodeURIComponent(varName)}`); + val === "" ? vals.push(ifEmpty) : vals.push("="); + } + vals.push(encodeValue(val, reserved, op)); + isFirst = false; + } + } else if (typeof value === "object") { + for (const key of Object.keys(value)) { + const val = value[key]; + if (!isDefined(val)) { + continue; + } + // prepare the following parts: separator, key, value + vals.push(`${getFirstOrSep(op, isFirst)}`); + if (key) { + vals.push(`${encodeURIComponent(key)}`); + named && val === "" ? vals.push(ifEmpty) : vals.push("="); + } + vals.push(encodeValue(val, reserved, op)); + isFirst = false; + } + } + return vals.join(""); +} + +function getNonExpandedValue(option: ValueOptions) { + const { op, varName, varValue: value, isFirst, reserved } = option; + const vals: string[] = []; + const first = getFirstOrSep(op, isFirst); + const [named, ifEmpty] = getNamedAndIfEmpty(op); + if (named && varName) { + vals.push(encodeValue(varName, reserved, op)); + if (value === "") { + if (!ifEmpty) { + vals.push(ifEmpty); + } + return !vals.join("") ? undefined : `${first}${vals.join("")}`; + } + vals.push("="); + } + + const items = []; + if (Array.isArray(value)) { + for (const val of value.filter(isDefined)) { + items.push(encodeValue(val, reserved, op)); + } + } else if (typeof value === "object") { + for (const key of Object.keys(value)) { + if (!isDefined(value[key])) { + continue; + } + items.push(encodeRFC3986URIComponent(key)); + items.push(encodeValue(value[key], reserved, op)); + } + } + vals.push(items.join(",")); + return !vals.join(",") ? undefined : `${first}${vals.join("")}`; +} + +function getVarValue(option: ValueOptions): string | undefined { + const { op, varName, modifier, isFirst, reserved, varValue: value } = option; + + if (!isDefined(value)) { + return undefined; + } else if (["string", "number", "boolean"].includes(typeof value)) { + let val = value.toString(); + const [named, ifEmpty] = getNamedAndIfEmpty(op); + const vals: string[] = [getFirstOrSep(op, isFirst)]; + if (named && varName) { + vals.push(encodeRFC3986URIComponent(varName)); + val === "" ? vals.push(ifEmpty) : vals.push("="); + } + if (modifier && modifier !== "*") { + val = val.substring(0, parseInt(modifier, 10)); + } + vals.push(encodeValue(val, reserved, op)); + return vals.join(""); + } else if (modifier === "*") { + return getExpandedValue(option); + } else { + return getNonExpandedValue(option); + } +} + +// --------------------------------------------------------------------------------------------------- +// This is an implementation of RFC 6570 URI Template: https://datatracker.ietf.org/doc/html/rfc6570. +// --------------------------------------------------------------------------------------------------- +export function expandUrlTemplate( + template: string, + context: Record, + option?: UrlTemplateOptions, +): string { + return template.replace(/\{([^\{\}]+)\}|([^\{\}]+)/g, (_, expr, text) => { + if (!expr) { + return encodeWithReserved(text); + } + let op; + if (["+", "#", ".", "/", ";", "?", "&"].includes(expr[0])) { + (op = expr[0]), (expr = expr.slice(1)); + } + const varList = expr.split(/,/g); + const result = []; + for (const varSpec of varList) { + const varMatch = /([^:\*]*)(?::(\d+)|(\*))?/.exec(varSpec); + if (!varMatch || !varMatch[1]) { + continue; + } + const varValue = getVarValue({ + isFirst: result.length === 0, + op, + varValue: context[varMatch[1]], + varName: varMatch[1], + modifier: varMatch[2] || varMatch[3], + reserved: option?.allowReserved ?? false, + }); + if (varValue) { + result.push(varValue); + } + } + return result.join(""); + }); +} diff --git a/packages/typespec-ts/test/commands/cadl-ranch-list.js b/packages/typespec-ts/test/commands/cadl-ranch-list.js index 1664efa44c..a3efca65f4 100644 --- a/packages/typespec-ts/test/commands/cadl-ranch-list.js +++ b/packages/typespec-ts/test/commands/cadl-ranch-list.js @@ -306,6 +306,10 @@ export const nonBrandedRlcTsps = [ ]; export const modularTsps = [ + { + outputPath: "routes", + inputPath: "routes" + }, { outputPath: "azure/client-generator-core/flatten-property", inputPath: "azure/client-generator-core/flatten-property" diff --git a/packages/typespec-ts/test/modularIntegration/azureCore.spec.ts b/packages/typespec-ts/test/modularIntegration/azureCore.spec.ts index af33a2a059..da0c2a8bc9 100644 --- a/packages/typespec-ts/test/modularIntegration/azureCore.spec.ts +++ b/packages/typespec-ts/test/modularIntegration/azureCore.spec.ts @@ -14,6 +14,27 @@ describe("BasicClient Classical Client", () => { describe("list", () => { describe("next", () => { it("should list all users", async () => { + const iter = client.list({ + top: 5, + skip: 10, + orderby: ["id"], + filter: "id lt 10", + select: ["id", "orders", "etag"], + expand: ["orders"] + }); + const items = []; + for await (const user of iter) { + items.push(user); + } + assert.strictEqual(items.length, 2); + assert.strictEqual(items[0]?.name, "Madge"); + assert.strictEqual( + items[1]?.etag, + "11bdc430-65e8-45ad-81d9-8ffa60d55b5a" + ); + }); + + it("could pass the validation if enable skipUrlEncoding because of no special chars", async () => { const iter = client.list({ top: 5, skip: 10, @@ -44,8 +65,7 @@ describe("BasicClient Classical Client", () => { orderby: ["id"], filter: "id lt 10", select: ["id", "orders", "etag"], - expand: ["orders"], - requestOptions: { skipUrlEncoding: true } + expand: ["orders"] }); const pagedItems = iter.byPage(); const items: User[] = []; @@ -67,8 +87,7 @@ describe("BasicClient Classical Client", () => { orderby: ["id"], filter: "id lt 10", select: ["id", "orders", "etag"], - expand: ["orders"], - requestOptions: { skipUrlEncoding: true } + expand: ["orders"] }); const pagedIter = iter.byPage({ maxPageSize: 10 } as any); diff --git a/packages/typespec-ts/test/modularIntegration/collectionFormat.spec.ts b/packages/typespec-ts/test/modularIntegration/collectionFormat.spec.ts index a54f21a317..58dc91ffed 100644 --- a/packages/typespec-ts/test/modularIntegration/collectionFormat.spec.ts +++ b/packages/typespec-ts/test/modularIntegration/collectionFormat.spec.ts @@ -17,11 +17,7 @@ describe("CollectionFormatClient Classical Client", () => { }); it("should send multi format in query", async () => { - const result = await client.query.multi(["blue", "red", "green"], { - requestOptions: { - skipUrlEncoding: true - } - }); + const result = await client.query.multi(["blue", "red", "green"]); assert.strictEqual(result, undefined); }); diff --git a/packages/typespec-ts/test/modularIntegration/generated/routes/.gitignore b/packages/typespec-ts/test/modularIntegration/generated/routes/.gitignore new file mode 100644 index 0000000000..39220655cc --- /dev/null +++ b/packages/typespec-ts/test/modularIntegration/generated/routes/.gitignore @@ -0,0 +1,6 @@ +/** +!/src +/src/** +!/src/index.d.ts +!/.gitignore +!/tspconfig.yaml diff --git a/packages/typespec-ts/test/modularIntegration/generated/routes/src/index.d.ts b/packages/typespec-ts/test/modularIntegration/generated/routes/src/index.d.ts new file mode 100644 index 0000000000..98cafaa8e2 --- /dev/null +++ b/packages/typespec-ts/test/modularIntegration/generated/routes/src/index.d.ts @@ -0,0 +1,286 @@ +import { ClientOptions } from '@azure-rest/core-client'; +import { OperationOptions } from '@azure-rest/core-client'; +import { Pipeline } from '@azure/core-rest-pipeline'; + +export declare interface FixedOptionalParams extends OperationOptions { +} + +export declare interface InInterfaceFixedOptionalParams extends OperationOptions { +} + +export declare interface InInterfaceOperations { + fixed: (options?: InInterfaceFixedOptionalParams) => Promise; +} + +export declare interface PathParametersAnnotationOnlyOptionalParams extends OperationOptions { +} + +export declare interface PathParametersExplicitOptionalParams extends OperationOptions { +} + +export declare interface PathParametersLabelExpansionExplodeArrayOptionalParams extends OperationOptions { +} + +export declare interface PathParametersLabelExpansionExplodeOperations { + primitive: (param: string, options?: PathParametersLabelExpansionExplodePrimitiveOptionalParams) => Promise; + array: (param: string[], options?: PathParametersLabelExpansionExplodeArrayOptionalParams) => Promise; + record: (param: Record, options?: PathParametersLabelExpansionExplodeRecordOptionalParams) => Promise; +} + +export declare interface PathParametersLabelExpansionExplodePrimitiveOptionalParams extends OperationOptions { +} + +export declare interface PathParametersLabelExpansionExplodeRecordOptionalParams extends OperationOptions { +} + +export declare interface PathParametersLabelExpansionOperations { + standard: PathParametersLabelExpansionStandardOperations; + explode: PathParametersLabelExpansionExplodeOperations; +} + +export declare interface PathParametersLabelExpansionStandardArrayOptionalParams extends OperationOptions { +} + +export declare interface PathParametersLabelExpansionStandardOperations { + primitive: (param: string, options?: PathParametersLabelExpansionStandardPrimitiveOptionalParams) => Promise; + array: (param: string[], options?: PathParametersLabelExpansionStandardArrayOptionalParams) => Promise; + record: (param: Record, options?: PathParametersLabelExpansionStandardRecordOptionalParams) => Promise; +} + +export declare interface PathParametersLabelExpansionStandardPrimitiveOptionalParams extends OperationOptions { +} + +export declare interface PathParametersLabelExpansionStandardRecordOptionalParams extends OperationOptions { +} + +export declare interface PathParametersMatrixExpansionExplodeArrayOptionalParams extends OperationOptions { +} + +export declare interface PathParametersMatrixExpansionExplodeOperations { + primitive: (param: string, options?: PathParametersMatrixExpansionExplodePrimitiveOptionalParams) => Promise; + array: (param: string[], options?: PathParametersMatrixExpansionExplodeArrayOptionalParams) => Promise; + record: (param: Record, options?: PathParametersMatrixExpansionExplodeRecordOptionalParams) => Promise; +} + +export declare interface PathParametersMatrixExpansionExplodePrimitiveOptionalParams extends OperationOptions { +} + +export declare interface PathParametersMatrixExpansionExplodeRecordOptionalParams extends OperationOptions { +} + +export declare interface PathParametersMatrixExpansionOperations { + standard: PathParametersMatrixExpansionStandardOperations; + explode: PathParametersMatrixExpansionExplodeOperations; +} + +export declare interface PathParametersMatrixExpansionStandardArrayOptionalParams extends OperationOptions { +} + +export declare interface PathParametersMatrixExpansionStandardOperations { + primitive: (param: string, options?: PathParametersMatrixExpansionStandardPrimitiveOptionalParams) => Promise; + array: (param: string[], options?: PathParametersMatrixExpansionStandardArrayOptionalParams) => Promise; + record: (param: Record, options?: PathParametersMatrixExpansionStandardRecordOptionalParams) => Promise; +} + +export declare interface PathParametersMatrixExpansionStandardPrimitiveOptionalParams extends OperationOptions { +} + +export declare interface PathParametersMatrixExpansionStandardRecordOptionalParams extends OperationOptions { +} + +export declare interface PathParametersOperations { + templateOnly: (param: string, options?: PathParametersTemplateOnlyOptionalParams) => Promise; + explicit: (param: string, options?: PathParametersExplicitOptionalParams) => Promise; + annotationOnly: (param: string, options?: PathParametersAnnotationOnlyOptionalParams) => Promise; + reservedExpansion: PathParametersReservedExpansionOperations; + simpleExpansion: PathParametersSimpleExpansionOperations; + pathExpansion: PathParametersPathExpansionOperations; + labelExpansion: PathParametersLabelExpansionOperations; + matrixExpansion: PathParametersMatrixExpansionOperations; +} + +export declare interface PathParametersPathExpansionExplodeArrayOptionalParams extends OperationOptions { +} + +export declare interface PathParametersPathExpansionExplodeOperations { + primitive: (param: string, options?: PathParametersPathExpansionExplodePrimitiveOptionalParams) => Promise; + array: (param: string[], options?: PathParametersPathExpansionExplodeArrayOptionalParams) => Promise; + record: (param: Record, options?: PathParametersPathExpansionExplodeRecordOptionalParams) => Promise; +} + +export declare interface PathParametersPathExpansionExplodePrimitiveOptionalParams extends OperationOptions { +} + +export declare interface PathParametersPathExpansionExplodeRecordOptionalParams extends OperationOptions { +} + +export declare interface PathParametersPathExpansionOperations { + standard: PathParametersPathExpansionStandardOperations; + explode: PathParametersPathExpansionExplodeOperations; +} + +export declare interface PathParametersPathExpansionStandardArrayOptionalParams extends OperationOptions { +} + +export declare interface PathParametersPathExpansionStandardOperations { + primitive: (param: string, options?: PathParametersPathExpansionStandardPrimitiveOptionalParams) => Promise; + array: (param: string[], options?: PathParametersPathExpansionStandardArrayOptionalParams) => Promise; + record: (param: Record, options?: PathParametersPathExpansionStandardRecordOptionalParams) => Promise; +} + +export declare interface PathParametersPathExpansionStandardPrimitiveOptionalParams extends OperationOptions { +} + +export declare interface PathParametersPathExpansionStandardRecordOptionalParams extends OperationOptions { +} + +export declare interface PathParametersReservedExpansionAnnotationOptionalParams extends OperationOptions { +} + +export declare interface PathParametersReservedExpansionOperations { + template: (param: string, options?: PathParametersReservedExpansionTemplateOptionalParams) => Promise; + annotation: (param: string, options?: PathParametersReservedExpansionAnnotationOptionalParams) => Promise; +} + +export declare interface PathParametersReservedExpansionTemplateOptionalParams extends OperationOptions { +} + +export declare interface PathParametersSimpleExpansionExplodeArrayOptionalParams extends OperationOptions { +} + +export declare interface PathParametersSimpleExpansionExplodeOperations { + primitive: (param: string, options?: PathParametersSimpleExpansionExplodePrimitiveOptionalParams) => Promise; + array: (param: string[], options?: PathParametersSimpleExpansionExplodeArrayOptionalParams) => Promise; + record: (param: Record, options?: PathParametersSimpleExpansionExplodeRecordOptionalParams) => Promise; +} + +export declare interface PathParametersSimpleExpansionExplodePrimitiveOptionalParams extends OperationOptions { +} + +export declare interface PathParametersSimpleExpansionExplodeRecordOptionalParams extends OperationOptions { +} + +export declare interface PathParametersSimpleExpansionOperations { + standard: PathParametersSimpleExpansionStandardOperations; + explode: PathParametersSimpleExpansionExplodeOperations; +} + +export declare interface PathParametersSimpleExpansionStandardArrayOptionalParams extends OperationOptions { +} + +export declare interface PathParametersSimpleExpansionStandardOperations { + primitive: (param: string, options?: PathParametersSimpleExpansionStandardPrimitiveOptionalParams) => Promise; + array: (param: string[], options?: PathParametersSimpleExpansionStandardArrayOptionalParams) => Promise; + record: (param: Record, options?: PathParametersSimpleExpansionStandardRecordOptionalParams) => Promise; +} + +export declare interface PathParametersSimpleExpansionStandardPrimitiveOptionalParams extends OperationOptions { +} + +export declare interface PathParametersSimpleExpansionStandardRecordOptionalParams extends OperationOptions { +} + +export declare interface PathParametersTemplateOnlyOptionalParams extends OperationOptions { +} + +export declare interface QueryParametersAnnotationOnlyOptionalParams extends OperationOptions { +} + +export declare interface QueryParametersExplicitOptionalParams extends OperationOptions { +} + +export declare interface QueryParametersOperations { + templateOnly: (param: string, options?: QueryParametersTemplateOnlyOptionalParams) => Promise; + explicit: (param: string, options?: QueryParametersExplicitOptionalParams) => Promise; + annotationOnly: (param: string, options?: QueryParametersAnnotationOnlyOptionalParams) => Promise; + queryExpansion: QueryParametersQueryExpansionOperations; + queryContinuation: QueryParametersQueryContinuationOperations; +} + +export declare interface QueryParametersQueryContinuationExplodeArrayOptionalParams extends OperationOptions { +} + +export declare interface QueryParametersQueryContinuationExplodeOperations { + primitive: (param: string, options?: QueryParametersQueryContinuationExplodePrimitiveOptionalParams) => Promise; + array: (param: string[], options?: QueryParametersQueryContinuationExplodeArrayOptionalParams) => Promise; + record: (param: Record, options?: QueryParametersQueryContinuationExplodeRecordOptionalParams) => Promise; +} + +export declare interface QueryParametersQueryContinuationExplodePrimitiveOptionalParams extends OperationOptions { +} + +export declare interface QueryParametersQueryContinuationExplodeRecordOptionalParams extends OperationOptions { +} + +export declare interface QueryParametersQueryContinuationOperations { + standard: QueryParametersQueryContinuationStandardOperations; + explode: QueryParametersQueryContinuationExplodeOperations; +} + +export declare interface QueryParametersQueryContinuationStandardArrayOptionalParams extends OperationOptions { +} + +export declare interface QueryParametersQueryContinuationStandardOperations { + primitive: (param: string, options?: QueryParametersQueryContinuationStandardPrimitiveOptionalParams) => Promise; + array: (param: string[], options?: QueryParametersQueryContinuationStandardArrayOptionalParams) => Promise; + record: (param: Record, options?: QueryParametersQueryContinuationStandardRecordOptionalParams) => Promise; +} + +export declare interface QueryParametersQueryContinuationStandardPrimitiveOptionalParams extends OperationOptions { +} + +export declare interface QueryParametersQueryContinuationStandardRecordOptionalParams extends OperationOptions { +} + +export declare interface QueryParametersQueryExpansionExplodeArrayOptionalParams extends OperationOptions { +} + +export declare interface QueryParametersQueryExpansionExplodeOperations { + primitive: (param: string, options?: QueryParametersQueryExpansionExplodePrimitiveOptionalParams) => Promise; + array: (param: string[], options?: QueryParametersQueryExpansionExplodeArrayOptionalParams) => Promise; + record: (param: Record, options?: QueryParametersQueryExpansionExplodeRecordOptionalParams) => Promise; +} + +export declare interface QueryParametersQueryExpansionExplodePrimitiveOptionalParams extends OperationOptions { +} + +export declare interface QueryParametersQueryExpansionExplodeRecordOptionalParams extends OperationOptions { +} + +export declare interface QueryParametersQueryExpansionOperations { + standard: QueryParametersQueryExpansionStandardOperations; + explode: QueryParametersQueryExpansionExplodeOperations; +} + +export declare interface QueryParametersQueryExpansionStandardArrayOptionalParams extends OperationOptions { +} + +export declare interface QueryParametersQueryExpansionStandardOperations { + primitive: (param: string, options?: QueryParametersQueryExpansionStandardPrimitiveOptionalParams) => Promise; + array: (param: string[], options?: QueryParametersQueryExpansionStandardArrayOptionalParams) => Promise; + record: (param: Record, options?: QueryParametersQueryExpansionStandardRecordOptionalParams) => Promise; +} + +export declare interface QueryParametersQueryExpansionStandardPrimitiveOptionalParams extends OperationOptions { +} + +export declare interface QueryParametersQueryExpansionStandardRecordOptionalParams extends OperationOptions { +} + +export declare interface QueryParametersTemplateOnlyOptionalParams extends OperationOptions { +} + +export declare class RoutesClient { + private _client; + readonly pipeline: Pipeline; + constructor(options?: RoutesClientOptionalParams); + fixed(options?: FixedOptionalParams): Promise; + readonly pathParameters: PathParametersOperations; + readonly queryParameters: QueryParametersOperations; + readonly inInterface: InInterfaceOperations; +} + +export declare interface RoutesClientOptionalParams extends ClientOptions { +} + +export { } diff --git a/packages/typespec-ts/test/modularIntegration/generated/routes/tspconfig.yaml b/packages/typespec-ts/test/modularIntegration/generated/routes/tspconfig.yaml new file mode 100644 index 0000000000..a9cbf6fa88 --- /dev/null +++ b/packages/typespec-ts/test/modularIntegration/generated/routes/tspconfig.yaml @@ -0,0 +1,13 @@ +emit: + - "@azure-tools/typespec-ts" +options: + "@azure-tools/typespec-ts": + "emitter-output-dir": "{project-root}" + generateMetadata: true + generateTest: false + azureSdkForJs: false + isTypeSpecTest: true + isModularLibrary: true + hierarchyClient: true + packageDetails: + name: "@msinternal/routes" diff --git a/packages/typespec-ts/test/modularIntegration/routes.spec.ts b/packages/typespec-ts/test/modularIntegration/routes.spec.ts new file mode 100644 index 0000000000..8ee7474846 --- /dev/null +++ b/packages/typespec-ts/test/modularIntegration/routes.spec.ts @@ -0,0 +1,214 @@ +import { RoutesClient } from "./generated/routes/src/index.js"; +import { assert } from "chai"; +describe("Routes Client", () => { + let client: RoutesClient; + + beforeEach(() => { + client = new RoutesClient({ + allowInsecureConnection: true, + endpoint: "http://localhost:3002" + }); + }); + + it("Routes_InInterface", async () => { + const result = await client.inInterface.fixed(); + assert.isUndefined(result); + }); + + it("Routes_fixed", async () => { + const result = await client.fixed(); + assert.isUndefined(result); + }); + + it("Routes_PathParameters_templateOnly", async () => { + const result = await client.pathParameters.templateOnly("a"); + assert.isUndefined(result); + }); + + it("Routes_PathParameters_explicit", async () => { + const result = await client.pathParameters.explicit("a"); + assert.isUndefined(result); + }); + it("Routes_PathParameters_annotationOnly", async () => { + const result = await client.pathParameters.annotationOnly("a"); + assert.isUndefined(result); + }); + it("Routes_PathParameters_ReservedExpansion_template", async () => { + const result = await client.pathParameters.reservedExpansion.template("foo/bar%20baz"); + assert.isUndefined(result); + }); + it("Routes_PathParameters_ReservedExpansion_annotation", async () => { + const result = await client.pathParameters.reservedExpansion.annotation("foo/bar%20baz"); + assert.isUndefined(result); + }); + // TODO: enable these cases when cadl-ranch path issue is fixed + describe.skip("Skip these cases", () => { + it("Routes_PathParameters_SimpleExpansion_Standard_primitive", async () => { + const result = await client.pathParameters.simpleExpansion.standard.primitive("a"); + assert.isUndefined(result); + }); + it("Routes_PathParameters_SimpleExpansion_Standard_array", async () => { + const result = await client.pathParameters.simpleExpansion.standard.array(["a", "b"]); + assert.isUndefined(result); + }); + it("Routes_PathParameters_SimpleExpansion_Standard_record", async () => { + const result = await client.pathParameters.simpleExpansion.standard.record({ a: 1, b: 2 }); + assert.isUndefined(result); + }); + it("Routes_PathParameters_SimpleExpansion_Explode_primitive", async () => { + const result = await client.pathParameters.simpleExpansion.explode.primitive("a"); + assert.isUndefined(result); + }); + + it("Routes_PathParameters_SimpleExpansion_Explode_primitive", async () => { + const result = await client.pathParameters.simpleExpansion.explode.primitive("a"); + assert.isUndefined(result); + }); + it("Routes_PathParameters_SimpleExpansion_Explode_array", async () => { + const result = await client.pathParameters.simpleExpansion.explode.array(["a", "b"]); + assert.isUndefined(result); + }); + it("Routes_PathParameters_SimpleExpansion_Explode_record", async () => { + const result = await client.pathParameters.simpleExpansion.explode.record({ a: 1, b: 2 }); + assert.isUndefined(result); + }); + + it("Routes_PathParameters_PathExpansion_Standard_primitive", async () => { + const result = await client.pathParameters.pathExpansion.standard.primitive("a"); + assert.isUndefined(result); + }); + + it("Routes_PathParameters_PathExpansion_Standard_array", async () => { + const result = await client.pathParameters.pathExpansion.standard.array(["a", "b"]); + assert.isUndefined(result); + }); + + it("Routes_PathParameters_PathExpansion_Standard_record", async () => { + const result = await client.pathParameters.pathExpansion.standard.record({ a: 1, b: 2 }); + assert.isUndefined(result); + }); + it("Routes_PathParameters_PathExpansion_Explode_primitive", async () => { + const result = await client.pathParameters.pathExpansion.explode.primitive("a"); + assert.isUndefined(result); + }); + it("Routes_PathParameters_PathExpansion_Explode_array", async () => { + const result = await client.pathParameters.pathExpansion.explode.array(["a", "b"]); + assert.isUndefined(result); + }); + it("Routes_PathParameters_PathExpansion_Explode_record", async () => { + const result = await client.pathParameters.pathExpansion.explode.record({ a: 1, b: 2 }); + assert.isUndefined(result); + }); + + it("Routes_PathParameters_LabelExpansion_Standard_primitive", async () => { + const result = await client.pathParameters.labelExpansion.standard.primitive("a"); + assert.isUndefined(result); + }); + + it("Routes_PathParameters_LabelExpansion_Standard_array", async () => { + const result = await client.pathParameters.labelExpansion.standard.array(["a", "b"]); + assert.isUndefined(result); + }); + it("Routes_PathParameters_LabelExpansion_Standard_record", async () => { + const result = await client.pathParameters.labelExpansion.standard.record({ a: 1, b: 2 }); + assert.isUndefined(result); + }); + it("Routes_PathParameters_LabelExpansion_Explode_primitive", async () => { + const result = await client.pathParameters.labelExpansion.explode.primitive("a"); + assert.isUndefined(result); + }); + it("Routes_PathParameters_LabelExpansion_Explode_array", async () => { + const result = await client.pathParameters.labelExpansion.explode.array(["a", "b"]); + assert.isUndefined(result); + }); + it("Routes_PathParameters_LabelExpansion_Explode_record", async () => { + const result = await client.pathParameters.labelExpansion.explode.record({ a: 1, b: 2 }); + assert.isUndefined(result); + }); + it("Routes_PathParameters_MatrixExpansion_Standard_primitive", async () => { + const result = await client.pathParameters.matrixExpansion.standard.primitive("a"); + assert.isUndefined(result); + }); + it("Routes_PathParameters_MatrixExpansion_Standard_array", async () => { + const result = await client.pathParameters.matrixExpansion.standard.array(["a", "b"]); + assert.isUndefined(result); + }); + it("Routes_PathParameters_MatrixExpansion_Standard_record", async () => { + const result = await client.pathParameters.matrixExpansion.standard.record({ a: 1, b: 2 }); + assert.isUndefined(result); + }); + it("Routes_PathParameters_MatrixExpansion_Explode_primitive", async () => { + const result = await client.pathParameters.matrixExpansion.explode.primitive("a"); + assert.isUndefined(result); + }); + it("Routes_PathParameters_MatrixExpansion_Explode_array", async () => { + const result = await client.pathParameters.matrixExpansion.explode.array(["a", "b"]); + assert.isUndefined(result); + }); + it("Routes_PathParameters_MatrixExpansion_Explode_record", async () => { + const result = await client.pathParameters.matrixExpansion.explode.record({ a: 1, b: 2 }); + assert.isUndefined(result); + }); + }); + + it("Routes_QueryParameters_templateOnly", async () => { + const result = await client.queryParameters.templateOnly("a"); + assert.isUndefined(result); + }); + it("Routes_QueryParameters_explicit", async () => { + const result = await client.queryParameters.explicit("a"); + assert.isUndefined(result); + }); + it("Routes_QueryParameters_annotationOnly", async () => { + const result = await client.queryParameters.annotationOnly("a"); + assert.isUndefined(result); + }); + it("Routes_QueryParameters_QueryExpansion_Standard_primitive", async () => { + const result = await client.queryParameters.queryExpansion.standard.primitive("a"); + assert.isUndefined(result); + }); + it("Routes_QueryParameters_QueryExpansion_Standard_array", async () => { + const result = await client.queryParameters.queryExpansion.standard.array(["a", "b"]); + assert.isUndefined(result); + }); + it("Routes_QueryParameters_QueryExpansion_Standard_record", async () => { + const result = await client.queryParameters.queryExpansion.standard.record({ a: 1, b: 2 }); + assert.isUndefined(result); + }); + it("Routes_QueryParameters_QueryExpansion_Explode_primitive", async () => { + const result = await client.queryParameters.queryExpansion.explode.primitive("a"); + assert.isUndefined(result); + }); + it("outes_QueryParameters_QueryExpansion_Explode_array", async () => { + const result = await client.queryParameters.queryExpansion.explode.array(["a", "b"]); + assert.isUndefined(result); + }); + it("Routes_QueryParameters_QueryExpansion_Explode_record", async () => { + const result = await client.queryParameters.queryExpansion.explode.record({ a: 1, b: 2 }); + assert.isUndefined(result); + }); + it("Routes_QueryParameters_QueryContinuation_Standard_primitive", async () => { + const result = await client.queryParameters.queryContinuation.standard.primitive("a"); + assert.isUndefined(result); + }); + it("Routes_QueryParameters_QueryContinuation_Standard_array", async () => { + const result = await client.queryParameters.queryContinuation.standard.array(["a", "b"]); + assert.isUndefined(result); + }); + it("Routes_QueryParameters_QueryContinuation_Standard_record", async () => { + const result = await client.queryParameters.queryContinuation.standard.record({ a: 1, b: 2 }); + assert.isUndefined(result); + }); + it("Routes_QueryParameters_QueryContinuation_Explode_primitive", async () => { + const result = await client.queryParameters.queryContinuation.explode.primitive("a"); + assert.isUndefined(result); + }); + it("outes_QueryParameters_QueryContinuation_Explode_array", async () => { + const result = await client.queryParameters.queryContinuation.explode.array(["a", "b"]); + assert.isUndefined(result); + }); + it("Routes_QueryParameters_QueryContinuation_Explode_record", async () => { + const result = await client.queryParameters.queryContinuation.explode.record({ a: 1, b: 2 }); + assert.isUndefined(result); + }); +}); diff --git a/packages/typespec-ts/test/modularUnit/scenarios/anonymous/anonymous.md b/packages/typespec-ts/test/modularUnit/scenarios/anonymous/anonymous.md index 9db8741995..6c22277418 100644 --- a/packages/typespec-ts/test/modularUnit/scenarios/anonymous/anonymous.md +++ b/packages/typespec-ts/test/modularUnit/scenarios/anonymous/anonymous.md @@ -42,11 +42,12 @@ export function barSerializer(item: Bar): any { ```ts operations import { TestingContext as Client } from "./index.js"; import { Bar } from "../models/models.js"; +import { expandUrlTemplate } from "../static-helpers/urlTemplate.js"; import { StreamableMethod, PathUncheckedResponse, createRestError, - operationOptionsToRequestParameters + operationOptionsToRequestParameters, } from "@azure-rest/core-client"; export function _readSend( @@ -58,23 +59,34 @@ export function _readSend( prop3: Date, prop4: string, prop5: Bar, - options: ReadOptionalParams = { requestOptions: {} } + options: ReadOptionalParams = { requestOptions: {} }, ): StreamableMethod { - return context.path("/{pathParam}", pathParam).post({ - ...operationOptionsToRequestParameters(options), - queryParameters: { queryParam: queryParam }, - body: { - prop1: prop1, - prop2: prop2, - prop3: prop3.toISOString(), - prop4: prop4, - prop5: { prop1: prop5["prop1"], prop2: prop5["prop2"] } - } - }); + const path = expandUrlTemplate( + "/{pathParam}{?queryParam}", + { + pathParam: pathParam, + queryParam: queryParam, + }, + { + allowReserved: options?.requestOptions?.skipUrlEncoding, + }, + ); + return context + .path(path) + .post({ + ...operationOptionsToRequestParameters(options), + body: { + prop1: prop1, + prop2: prop2, + prop3: prop3.toISOString(), + prop4: prop4, + prop5: { prop1: prop5["prop1"], prop2: prop5["prop2"] }, + }, + }); } export async function _readDeserialize( - result: PathUncheckedResponse + result: PathUncheckedResponse, ): Promise { const expectedStatuses = ["200"]; if (!expectedStatuses.includes(result.status)) { @@ -93,7 +105,7 @@ export async function read( prop3: Date, prop4: string, prop5: Bar, - options: ReadOptionalParams = { requestOptions: {} } + options: ReadOptionalParams = { requestOptions: {} }, ): Promise { const result = await _readSend( context, @@ -104,7 +116,7 @@ export async function read( prop3, prop4, prop5, - options + options, ); return _readDeserialize(result); } @@ -164,11 +176,12 @@ export interface ReadOptionalParams extends OperationOptions { ```ts operations import { TestingContext as Client } from "./index.js"; +import { expandUrlTemplate } from "../static-helpers/urlTemplate.js"; import { StreamableMethod, PathUncheckedResponse, createRestError, - operationOptionsToRequestParameters + operationOptionsToRequestParameters, } from "@azure-rest/core-client"; export function _readSend( @@ -178,26 +191,37 @@ export function _readSend( prop1: string, prop2: number, prop4: string, - options: ReadOptionalParams = { requestOptions: {} } + options: ReadOptionalParams = { requestOptions: {} }, ): StreamableMethod { - return context.path("/{pathParam}", pathParam).post({ - ...operationOptionsToRequestParameters(options), - queryParameters: { queryParam: queryParam }, - body: { - prop1: prop1, - prop2: prop2, - prop3: options?.prop3?.toISOString(), - prop4: prop4, - prop5: { - prop1: options?.prop5?.["prop1"], - prop2: options?.prop5?.["prop2"] - } - } - }); + const path = expandUrlTemplate( + "/{pathParam}{?queryParam}", + { + pathParam: pathParam, + queryParam: queryParam, + }, + { + allowReserved: options?.requestOptions?.skipUrlEncoding, + }, + ); + return context + .path(path) + .post({ + ...operationOptionsToRequestParameters(options), + body: { + prop1: prop1, + prop2: prop2, + prop3: options?.prop3?.toISOString(), + prop4: prop4, + prop5: { + prop1: options?.prop5?.["prop1"], + prop2: options?.prop5?.["prop2"], + }, + }, + }); } export async function _readDeserialize( - result: PathUncheckedResponse + result: PathUncheckedResponse, ): Promise { const expectedStatuses = ["200"]; if (!expectedStatuses.includes(result.status)) { @@ -214,7 +238,7 @@ export async function read( prop1: string, prop2: number, prop4: string, - options: ReadOptionalParams = { requestOptions: {} } + options: ReadOptionalParams = { requestOptions: {} }, ): Promise { const result = await _readSend( context, @@ -223,7 +247,7 @@ export async function read( prop1, prop2, prop4, - options + options, ); return _readDeserialize(result); } @@ -285,11 +309,12 @@ export interface ReadOptionalParams extends OperationOptions { ```ts operations import { TestingContext as Client } from "./index.js"; +import { expandUrlTemplate } from "../static-helpers/urlTemplate.js"; import { StreamableMethod, PathUncheckedResponse, createRestError, - operationOptionsToRequestParameters + operationOptionsToRequestParameters, } from "@azure-rest/core-client"; export function _readSend( @@ -299,24 +324,37 @@ export function _readSend( prop4: string, queryParam: string, prop2: number, - options: ReadOptionalParams = { requestOptions: {} } + options: ReadOptionalParams = { requestOptions: {} }, ): StreamableMethod { - return context.path("/{pathParam}/{prop1}", pathParam, prop1).post({ - ...operationOptionsToRequestParameters(options), - queryParameters: { prop4: prop4, queryParam: queryParam }, - body: { - prop2: prop2, - prop3: options?.prop3?.toISOString(), - prop5: { - prop1: options?.prop5?.["prop1"], - prop2: options?.prop5?.["prop2"] - } - } - }); + const path = expandUrlTemplate( + "/{pathParam}/{prop1}{?prop4,queryParam}", + { + pathParam: pathParam, + prop1: prop1, + prop4: prop4, + queryParam: queryParam, + }, + { + allowReserved: options?.requestOptions?.skipUrlEncoding, + }, + ); + return context + .path(path) + .post({ + ...operationOptionsToRequestParameters(options), + body: { + prop2: prop2, + prop3: options?.prop3?.toISOString(), + prop5: { + prop1: options?.prop5?.["prop1"], + prop2: options?.prop5?.["prop2"], + }, + }, + }); } export async function _readDeserialize( - result: PathUncheckedResponse + result: PathUncheckedResponse, ): Promise { const expectedStatuses = ["200"]; if (!expectedStatuses.includes(result.status)) { @@ -333,7 +371,7 @@ export async function read( prop4: string, queryParam: string, prop2: number, - options: ReadOptionalParams = { requestOptions: {} } + options: ReadOptionalParams = { requestOptions: {} }, ): Promise { const result = await _readSend( context, @@ -342,7 +380,7 @@ export async function read( prop4, queryParam, prop2, - options + options, ); return _readDeserialize(result); } @@ -407,7 +445,7 @@ export function fooSerializer(item: Foo): any { prop2: item["prop2"], prop3: item["prop3"].toISOString(), prop4: item["prop4"], - prop5: barSerializer(item["prop5"]) + prop5: barSerializer(item["prop5"]), }; } ``` @@ -417,11 +455,12 @@ export function fooSerializer(item: Foo): any { ```ts operations import { TestingContext as Client } from "./index.js"; import { Foo, fooSerializer } from "../models/models.js"; +import { expandUrlTemplate } from "../static-helpers/urlTemplate.js"; import { StreamableMethod, PathUncheckedResponse, createRestError, - operationOptionsToRequestParameters + operationOptionsToRequestParameters, } from "@azure-rest/core-client"; export function _readSend( @@ -429,17 +468,28 @@ export function _readSend( pathParam: string, queryParam: string, body: Foo, - options: ReadOptionalParams = { requestOptions: {} } + options: ReadOptionalParams = { requestOptions: {} }, ): StreamableMethod { - return context.path("/{pathParam}", pathParam).post({ - ...operationOptionsToRequestParameters(options), - queryParameters: { queryParam: queryParam }, - body: fooSerializer(body) - }); + const path = expandUrlTemplate( + "/{pathParam}{?queryParam}", + { + pathParam: pathParam, + queryParam: queryParam, + }, + { + allowReserved: options?.requestOptions?.skipUrlEncoding, + }, + ); + return context + .path(path) + .post({ + ...operationOptionsToRequestParameters(options), + body: fooSerializer(body), + }); } export async function _readDeserialize( - result: PathUncheckedResponse + result: PathUncheckedResponse, ): Promise { const expectedStatuses = ["200"]; if (!expectedStatuses.includes(result.status)) { @@ -454,7 +504,7 @@ export async function read( pathParam: string, queryParam: string, body: Foo, - options: ReadOptionalParams = { requestOptions: {} } + options: ReadOptionalParams = { requestOptions: {} }, ): Promise { const result = await _readSend(context, pathParam, queryParam, body, options); return _readDeserialize(result); @@ -485,11 +535,12 @@ export function _readRequestSerializer(item: _ReadRequest): any { ```ts operations import { TestingContext as Client } from "./index.js"; import { _readRequestSerializer } from "../models/models.js"; +import { expandUrlTemplate } from "../static-helpers/urlTemplate.js"; import { StreamableMethod, PathUncheckedResponse, createRestError, - operationOptionsToRequestParameters + operationOptionsToRequestParameters, } from "@azure-rest/core-client"; export function _readSend( @@ -497,17 +548,28 @@ export function _readSend( pathParam: string, queryParam: string, body: Record, - options: ReadOptionalParams = { requestOptions: {} } + options: ReadOptionalParams = { requestOptions: {} }, ): StreamableMethod { - return context.path("/{pathParam}", pathParam).post({ - ...operationOptionsToRequestParameters(options), - queryParameters: { queryParam: queryParam }, - body: _readRequestSerializer(body) - }); + const path = expandUrlTemplate( + "/{pathParam}{?queryParam}", + { + pathParam: pathParam, + queryParam: queryParam, + }, + { + allowReserved: options?.requestOptions?.skipUrlEncoding, + }, + ); + return context + .path(path) + .post({ + ...operationOptionsToRequestParameters(options), + body: _readRequestSerializer(body), + }); } export async function _readDeserialize( - result: PathUncheckedResponse + result: PathUncheckedResponse, ): Promise { const expectedStatuses = ["200"]; if (!expectedStatuses.includes(result.status)) { @@ -522,7 +584,7 @@ export async function read( pathParam: string, queryParam: string, body: Record, - options: ReadOptionalParams = { requestOptions: {} } + options: ReadOptionalParams = { requestOptions: {} }, ): Promise { const result = await _readSend(context, pathParam, queryParam, body, options); return _readDeserialize(result); @@ -567,11 +629,12 @@ export function barSerializer(item: Bar): any { ```ts operations import { TestingContext as Client } from "./index.js"; import { _readRequestSerializer, Bar } from "../models/models.js"; +import { expandUrlTemplate } from "../static-helpers/urlTemplate.js"; import { StreamableMethod, PathUncheckedResponse, createRestError, - operationOptionsToRequestParameters + operationOptionsToRequestParameters, } from "@azure-rest/core-client"; export function _readSend( @@ -582,17 +645,28 @@ export function _readSend( prop1: string; prop2: Bar; }, - options: ReadOptionalParams = { requestOptions: {} } + options: ReadOptionalParams = { requestOptions: {} }, ): StreamableMethod { - return context.path("/{pathParam}", pathParam).post({ - ...operationOptionsToRequestParameters(options), - queryParameters: { queryParam: queryParam }, - body: _readRequestSerializer(test) - }); + const path = expandUrlTemplate( + "/{pathParam}{?queryParam}", + { + pathParam: pathParam, + queryParam: queryParam, + }, + { + allowReserved: options?.requestOptions?.skipUrlEncoding, + }, + ); + return context + .path(path) + .post({ + ...operationOptionsToRequestParameters(options), + body: _readRequestSerializer(test), + }); } export async function _readDeserialize( - result: PathUncheckedResponse + result: PathUncheckedResponse, ): Promise { const expectedStatuses = ["200"]; if (!expectedStatuses.includes(result.status)) { @@ -610,7 +684,7 @@ export async function read( prop1: string; prop2: Bar; }, - options: ReadOptionalParams = { requestOptions: {} } + options: ReadOptionalParams = { requestOptions: {} }, ): Promise { const result = await _readSend(context, pathParam, queryParam, test, options); return _readDeserialize(result); @@ -654,22 +728,24 @@ import { StreamableMethod, PathUncheckedResponse, createRestError, - operationOptionsToRequestParameters + operationOptionsToRequestParameters, } from "@azure-rest/core-client"; export function _readSend( context: Client, body: Test, - options: ReadOptionalParams = { requestOptions: {} } + options: ReadOptionalParams = { requestOptions: {} }, ): StreamableMethod { - return context.path("/").post({ - ...operationOptionsToRequestParameters(options), - body: testSerializer(body) - }); + return context + .path("/") + .post({ + ...operationOptionsToRequestParameters(options), + body: testSerializer(body), + }); } export async function _readDeserialize( - result: PathUncheckedResponse + result: PathUncheckedResponse, ): Promise { const expectedStatuses = ["204"]; if (!expectedStatuses.includes(result.status)) { @@ -682,7 +758,7 @@ export async function _readDeserialize( export async function read( context: Client, body: Test, - options: ReadOptionalParams = { requestOptions: {} } + options: ReadOptionalParams = { requestOptions: {} }, ): Promise { const result = await _readSend(context, body, options); return _readDeserialize(result); @@ -730,22 +806,24 @@ import { StreamableMethod, PathUncheckedResponse, createRestError, - operationOptionsToRequestParameters + operationOptionsToRequestParameters, } from "@azure-rest/core-client"; export function _readSend( context: Client, body: Test, - options: ReadOptionalParams = { requestOptions: {} } + options: ReadOptionalParams = { requestOptions: {} }, ): StreamableMethod { - return context.path("/").post({ - ...operationOptionsToRequestParameters(options), - body: testSerializer(body) - }); + return context + .path("/") + .post({ + ...operationOptionsToRequestParameters(options), + body: testSerializer(body), + }); } export async function _readDeserialize( - result: PathUncheckedResponse + result: PathUncheckedResponse, ): Promise { const expectedStatuses = ["204"]; if (!expectedStatuses.includes(result.status)) { @@ -758,7 +836,7 @@ export async function _readDeserialize( export async function read( context: Client, body: Test, - options: ReadOptionalParams = { requestOptions: {} } + options: ReadOptionalParams = { requestOptions: {} }, ): Promise { const result = await _readSend(context, body, options); return _readDeserialize(result); @@ -793,12 +871,12 @@ import { StreamableMethod, PathUncheckedResponse, createRestError, - operationOptionsToRequestParameters + operationOptionsToRequestParameters, } from "@azure-rest/core-client"; export function _readSend( context: Client, - options: ReadOptionalParams = { requestOptions: {} } + options: ReadOptionalParams = { requestOptions: {} }, ): StreamableMethod { return context .path("/") @@ -806,7 +884,7 @@ export function _readSend( } export async function _readDeserialize( - result: PathUncheckedResponse + result: PathUncheckedResponse, ): Promise> { const expectedStatuses = ["200"]; if (!expectedStatuses.includes(result.status)) { @@ -818,7 +896,7 @@ export async function _readDeserialize( export async function read( context: Client, - options: ReadOptionalParams = { requestOptions: {} } + options: ReadOptionalParams = { requestOptions: {} }, ): Promise> { const result = await _readSend(context, options); return _readDeserialize(result); @@ -855,12 +933,12 @@ import { StreamableMethod, PathUncheckedResponse, createRestError, - operationOptionsToRequestParameters + operationOptionsToRequestParameters, } from "@azure-rest/core-client"; export function _readSend( context: Client, - options: ReadOptionalParams = { requestOptions: {} } + options: ReadOptionalParams = { requestOptions: {} }, ): StreamableMethod { return context .path("/") @@ -868,7 +946,7 @@ export function _readSend( } export async function _readDeserialize( - result: PathUncheckedResponse + result: PathUncheckedResponse, ): Promise { const expectedStatuses = ["200"]; if (!expectedStatuses.includes(result.status)) { @@ -880,7 +958,7 @@ export async function _readDeserialize( export async function read( context: Client, - options: ReadOptionalParams = { requestOptions: {} } + options: ReadOptionalParams = { requestOptions: {} }, ): Promise { const result = await _readSend(context, options); return _readDeserialize(result); @@ -909,7 +987,7 @@ export interface _ReadResponse { export function _readResponseDeserializer(item: any): _ReadResponse { return { - foo: !item["foo"] ? item["foo"] : _readResponseFooDeserializer(item["foo"]) + foo: !item["foo"] ? item["foo"] : _readResponseFooDeserializer(item["foo"]), }; } @@ -920,7 +998,7 @@ export interface _ReadResponseFoo { export function _readResponseFooDeserializer(item: any): _ReadResponseFoo { return { - bar: item["bar"] + bar: item["bar"], }; } ``` @@ -934,12 +1012,12 @@ import { StreamableMethod, PathUncheckedResponse, createRestError, - operationOptionsToRequestParameters + operationOptionsToRequestParameters, } from "@azure-rest/core-client"; export function _readSend( context: Client, - options: ReadOptionalParams = { requestOptions: {} } + options: ReadOptionalParams = { requestOptions: {} }, ): StreamableMethod { return context .path("/") @@ -961,7 +1039,7 @@ export async function _readDeserialize(result: PathUncheckedResponse): Promise<{ export async function read( context: Client, - options: ReadOptionalParams = { requestOptions: {} } + options: ReadOptionalParams = { requestOptions: {} }, ): Promise<{ foo?: { bar: string | null; @@ -1007,14 +1085,14 @@ export function returnBodyDeserializer(item: any): ReturnBody { return { emptyAnomyous: _returnBodyEmptyAnomyousDeserializer(item["emptyAnomyous"]), emptyAnomyousArray: returnBodyEmptyAnomyousArrayArrayDeserializer( - item["emptyAnomyousArray"] + item["emptyAnomyousArray"], ), emptyAnomyousDict: returnBodyEmptyAnomyousDictRecordDeserializer( - item["emptyAnomyousDict"] + item["emptyAnomyousDict"], ), emptyModel: emptyModelDeserializer(item["emptyModel"]), emptyModelArray: emptyModelArrayDeserializer(item["emptyModelArray"]), - emptyModelDict: emptyModelRecordDeserializer(item["emptyModelDict"]) + emptyModelDict: emptyModelRecordDeserializer(item["emptyModelDict"]), }; } @@ -1022,13 +1100,13 @@ export function returnBodyDeserializer(item: any): ReturnBody { export interface _ReturnBodyEmptyAnomyous {} export function _returnBodyEmptyAnomyousDeserializer( - item: any + item: any, ): _ReturnBodyEmptyAnomyous { return item; } export function returnBodyEmptyAnomyousArrayArrayDeserializer( - result: Array<_ReturnBodyEmptyAnomyousArray> + result: Array<_ReturnBodyEmptyAnomyousArray>, ): any[] { return result.map((item) => { return _returnBodyEmptyAnomyousArrayDeserializer(item); @@ -1039,13 +1117,13 @@ export function returnBodyEmptyAnomyousArrayArrayDeserializer( export interface _ReturnBodyEmptyAnomyousArray {} export function _returnBodyEmptyAnomyousArrayDeserializer( - item: any + item: any, ): _ReturnBodyEmptyAnomyousArray { return item; } export function returnBodyEmptyAnomyousDictRecordDeserializer( - item: Record + item: Record, ): Record { const result: Record = {}; Object.keys(item).map((key) => { @@ -1060,7 +1138,7 @@ export function returnBodyEmptyAnomyousDictRecordDeserializer( export interface _ReturnBodyEmptyAnomyousDict {} export function _returnBodyEmptyAnomyousDictDeserializer( - item: any + item: any, ): _ReturnBodyEmptyAnomyousDict { return item; } @@ -1079,7 +1157,7 @@ export function emptyModelArrayDeserializer(result: Array): any[] { } export function emptyModelRecordDeserializer( - item: Record + item: Record, ): Record { const result: Record = {}; Object.keys(item).map((key) => { @@ -1098,12 +1176,12 @@ import { StreamableMethod, PathUncheckedResponse, createRestError, - operationOptionsToRequestParameters + operationOptionsToRequestParameters, } from "@azure-rest/core-client"; export function _readSend( context: Client, - options: ReadOptionalParams = { requestOptions: {} } + options: ReadOptionalParams = { requestOptions: {} }, ): StreamableMethod { return context .path("/") @@ -1111,7 +1189,7 @@ export function _readSend( } export async function _readDeserialize( - result: PathUncheckedResponse + result: PathUncheckedResponse, ): Promise { const expectedStatuses = ["200"]; if (!expectedStatuses.includes(result.status)) { @@ -1123,7 +1201,7 @@ export async function _readDeserialize( export async function read( context: Client, - options: ReadOptionalParams = { requestOptions: {} } + options: ReadOptionalParams = { requestOptions: {} }, ): Promise { const result = await _readSend(context, options); return _readDeserialize(result); @@ -1178,7 +1256,7 @@ export interface Foz { export function fozDeserializer(item: any): Foz { return { - baz: _fozBazDeserializer(item["baz"]) + baz: _fozBazDeserializer(item["baz"]), }; } @@ -1211,19 +1289,19 @@ export function _fozBazDeserializer(item: any): _FozBaz { ? item["test"] : simpleModelArrayDeserializer(item["test"]), nonemptyAnomyous: _fozBazNonemptyAnomyousDeserializer( - item["nonemptyAnomyous"] + item["nonemptyAnomyous"], ), nonemptyAnomyousArray: fozBazNonemptyAnomyousArrayArrayDeserializer( - item["nonemptyAnomyousArray"] + item["nonemptyAnomyousArray"], ), nonemptyAnomyousDict: fozBazNonemptyAnomyousDictRecordDeserializer( - item["nonemptyAnomyousDict"] - ) + item["nonemptyAnomyousDict"], + ), }; } export function simpleModelArrayDeserializer( - result: Array + result: Array, ): any[] { return result.map((item) => { return simpleModelDeserializer(item); @@ -1237,7 +1315,7 @@ export interface SimpleModel { export function simpleModelDeserializer(item: any): SimpleModel { return { - test: item["test"] + test: item["test"], }; } @@ -1247,15 +1325,15 @@ export interface _FozBazNonemptyAnomyous { } export function _fozBazNonemptyAnomyousDeserializer( - item: any + item: any, ): _FozBazNonemptyAnomyous { return { - a: item["a"] + a: item["a"], }; } export function fozBazNonemptyAnomyousArrayArrayDeserializer( - result: Array<_FozBazNonemptyAnomyousArray> + result: Array<_FozBazNonemptyAnomyousArray>, ): any[] { return result.map((item) => { return _fozBazNonemptyAnomyousArrayDeserializer(item); @@ -1268,15 +1346,15 @@ export interface _FozBazNonemptyAnomyousArray { } export function _fozBazNonemptyAnomyousArrayDeserializer( - item: any + item: any, ): _FozBazNonemptyAnomyousArray { return { - b: item["b"] + b: item["b"], }; } export function fozBazNonemptyAnomyousDictRecordDeserializer( - item: Record + item: Record, ): Record { const result: Record = {}; Object.keys(item).map((key) => { @@ -1293,12 +1371,12 @@ export interface _FozBazNonemptyAnomyousDict { } export function _fozBazNonemptyAnomyousDictDeserializer( - item: any + item: any, ): _FozBazNonemptyAnomyousDict { return { c: item["c"].map((p: any) => { return p; - }) + }), }; } ``` @@ -1312,12 +1390,12 @@ import { StreamableMethod, PathUncheckedResponse, createRestError, - operationOptionsToRequestParameters + operationOptionsToRequestParameters, } from "@azure-rest/core-client"; export function _readSend( context: Client, - options: ReadOptionalParams = { requestOptions: {} } + options: ReadOptionalParams = { requestOptions: {} }, ): StreamableMethod { return context .path("/") @@ -1325,7 +1403,7 @@ export function _readSend( } export async function _readDeserialize( - result: PathUncheckedResponse + result: PathUncheckedResponse, ): Promise { const expectedStatuses = ["200"]; if (!expectedStatuses.includes(result.status)) { @@ -1337,7 +1415,7 @@ export async function _readDeserialize( export async function read( context: Client, - options: ReadOptionalParams = { requestOptions: {} } + options: ReadOptionalParams = { requestOptions: {} }, ): Promise { const result = await _readSend(context, options); return _readDeserialize(result); diff --git a/packages/typespec-ts/test/modularUnit/scenarios/apiOperations/apiOperations.md b/packages/typespec-ts/test/modularUnit/scenarios/apiOperations/apiOperations.md index 679d86f05f..73f700a4ed 100644 --- a/packages/typespec-ts/test/modularUnit/scenarios/apiOperations/apiOperations.md +++ b/packages/typespec-ts/test/modularUnit/scenarios/apiOperations/apiOperations.md @@ -20,23 +20,25 @@ import { StreamableMethod, PathUncheckedResponse, createRestError, - operationOptionsToRequestParameters + operationOptionsToRequestParameters, } from "@azure-rest/core-client"; export function _uploadFileViaBodySend( context: Client, body: Uint8Array, - options: UploadFileViaBodyOptionalParams = { requestOptions: {} } + options: UploadFileViaBodyOptionalParams = { requestOptions: {} }, ): StreamableMethod { - return context.path("/uploadFileViaBody").post({ - ...operationOptionsToRequestParameters(options), - contentType: (options.contentType as any) ?? "application/octet-stream", - body: body - }); + return context + .path("/uploadFileViaBody") + .post({ + ...operationOptionsToRequestParameters(options), + contentType: (options.contentType as any) ?? "application/octet-stream", + body: body, + }); } export async function _uploadFileViaBodyDeserialize( - result: PathUncheckedResponse + result: PathUncheckedResponse, ): Promise { const expectedStatuses = ["204"]; if (!expectedStatuses.includes(result.status)) { @@ -49,7 +51,7 @@ export async function _uploadFileViaBodyDeserialize( export async function uploadFileViaBody( context: Client, body: Uint8Array, - options: UploadFileViaBodyOptionalParams = { requestOptions: {} } + options: UploadFileViaBodyOptionalParams = { requestOptions: {} }, ): Promise { const result = await _uploadFileViaBodySend(context, body, options); return _uploadFileViaBodyDeserialize(result); @@ -79,23 +81,25 @@ import { StreamableMethod, PathUncheckedResponse, createRestError, - operationOptionsToRequestParameters + operationOptionsToRequestParameters, } from "@azure-rest/core-client"; export function _uploadFileViaBodySend( context: Client, body: Uint8Array, - options: UploadFileViaBodyOptionalParams = { requestOptions: {} } + options: UploadFileViaBodyOptionalParams = { requestOptions: {} }, ): StreamableMethod { - return context.path("/uploadFileViaBody").post({ - ...operationOptionsToRequestParameters(options), - contentType: (options.contentType as any) ?? "application/octet-stream", - body: body - }); + return context + .path("/uploadFileViaBody") + .post({ + ...operationOptionsToRequestParameters(options), + contentType: (options.contentType as any) ?? "application/octet-stream", + body: body, + }); } export async function _uploadFileViaBodyDeserialize( - result: PathUncheckedResponse + result: PathUncheckedResponse, ): Promise { const expectedStatuses = ["204"]; if (!expectedStatuses.includes(result.status)) { @@ -108,7 +112,7 @@ export async function _uploadFileViaBodyDeserialize( export async function uploadFileViaBody( context: Client, body: Uint8Array, - options: UploadFileViaBodyOptionalParams = { requestOptions: {} } + options: UploadFileViaBodyOptionalParams = { requestOptions: {} }, ): Promise { const result = await _uploadFileViaBodySend(context, body, options); return _uploadFileViaBodyDeserialize(result); @@ -146,7 +150,7 @@ export interface _UploadFileRequest { export function _uploadFileRequestSerializer(item: _UploadFileRequest): any { return { name: item["name"], - file: uint8ArrayToString(item["file"], "base64") + file: uint8ArrayToString(item["file"], "base64"), }; } ``` @@ -160,7 +164,7 @@ import { StreamableMethod, PathUncheckedResponse, createRestError, - operationOptionsToRequestParameters + operationOptionsToRequestParameters, } from "@azure-rest/core-client"; export function _uploadFileSend( @@ -169,17 +173,19 @@ export function _uploadFileSend( name: string; file: Uint8Array; }, - options: UploadFileOptionalParams = { requestOptions: {} } + options: UploadFileOptionalParams = { requestOptions: {} }, ): StreamableMethod { - return context.path("/uploadFile").post({ - ...operationOptionsToRequestParameters(options), - contentType: (options.contentType as any) ?? "multipart/form-data", - body: _uploadFileRequestSerializer(body) - }); + return context + .path("/uploadFile") + .post({ + ...operationOptionsToRequestParameters(options), + contentType: (options.contentType as any) ?? "multipart/form-data", + body: _uploadFileRequestSerializer(body), + }); } export async function _uploadFileDeserialize( - result: PathUncheckedResponse + result: PathUncheckedResponse, ): Promise { const expectedStatuses = ["204"]; if (!expectedStatuses.includes(result.status)) { @@ -195,7 +201,7 @@ export async function uploadFile( name: string; file: Uint8Array; }, - options: UploadFileOptionalParams = { requestOptions: {} } + options: UploadFileOptionalParams = { requestOptions: {} }, ): Promise { const result = await _uploadFileSend(context, body, options); return _uploadFileDeserialize(result); @@ -232,7 +238,7 @@ export function _uploadFilesRequestSerializer(item: _UploadFilesRequest): any { return { files: item["files"].map((p: any) => { return uint8ArrayToString(p, "base64"); - }) + }), }; } ``` @@ -246,7 +252,7 @@ import { StreamableMethod, PathUncheckedResponse, createRestError, - operationOptionsToRequestParameters + operationOptionsToRequestParameters, } from "@azure-rest/core-client"; export function _uploadFilesSend( @@ -254,17 +260,19 @@ export function _uploadFilesSend( body: { files: Uint8Array[]; }, - options: UploadFilesOptionalParams = { requestOptions: {} } + options: UploadFilesOptionalParams = { requestOptions: {} }, ): StreamableMethod { - return context.path("/uploadFiles").post({ - ...operationOptionsToRequestParameters(options), - contentType: (options.contentType as any) ?? "multipart/form-data", - body: _uploadFilesRequestSerializer(body) - }); + return context + .path("/uploadFiles") + .post({ + ...operationOptionsToRequestParameters(options), + contentType: (options.contentType as any) ?? "multipart/form-data", + body: _uploadFilesRequestSerializer(body), + }); } export async function _uploadFilesDeserialize( - result: PathUncheckedResponse + result: PathUncheckedResponse, ): Promise { const expectedStatuses = ["204"]; if (!expectedStatuses.includes(result.status)) { @@ -279,7 +287,7 @@ export async function uploadFiles( body: { files: Uint8Array[]; }, - options: UploadFilesOptionalParams = { requestOptions: {} } + options: UploadFilesOptionalParams = { requestOptions: {} }, ): Promise { const result = await _uploadFilesSend(context, body, options); return _uploadFilesDeserialize(result); @@ -307,12 +315,12 @@ import { StreamableMethod, PathUncheckedResponse, createRestError, - operationOptionsToRequestParameters + operationOptionsToRequestParameters, } from "@azure-rest/core-client"; export function _downloadFileSend( context: Client, - options: DownloadFileOptionalParams = { requestOptions: {} } + options: DownloadFileOptionalParams = { requestOptions: {} }, ): StreamableMethod { return context .path("/downloadFile") @@ -320,7 +328,7 @@ export function _downloadFileSend( } export async function _downloadFileDeserialize( - result: PathUncheckedResponse + result: PathUncheckedResponse, ): Promise { const expectedStatuses = ["200"]; if (!expectedStatuses.includes(result.status)) { @@ -332,7 +340,7 @@ export async function _downloadFileDeserialize( export async function downloadFile( context: Client, - options: DownloadFileOptionalParams = { requestOptions: {} } + options: DownloadFileOptionalParams = { requestOptions: {} }, ): Promise { const result = await _downloadFileSend(context, options); return _downloadFileDeserialize(result); @@ -363,12 +371,12 @@ import { StreamableMethod, PathUncheckedResponse, createRestError, - operationOptionsToRequestParameters + operationOptionsToRequestParameters, } from "@azure-rest/core-client"; export function _downloadFileSend( context: Client, - options: DownloadFileOptionalParams = { requestOptions: {} } + options: DownloadFileOptionalParams = { requestOptions: {} }, ): StreamableMethod { return context .path("/downloadFile") @@ -376,7 +384,7 @@ export function _downloadFileSend( } export async function _downloadFileDeserialize( - result: PathUncheckedResponse + result: PathUncheckedResponse, ): Promise { const expectedStatuses = ["200"]; if (!expectedStatuses.includes(result.status)) { @@ -388,7 +396,7 @@ export async function _downloadFileDeserialize( export async function downloadFile( context: Client, - options: DownloadFileOptionalParams = { requestOptions: {} } + options: DownloadFileOptionalParams = { requestOptions: {} }, ): Promise { const result = await _downloadFileSend(context, options); return _downloadFileDeserialize(result); @@ -423,14 +431,14 @@ export interface _DownloadFileResponse { } export function _downloadFileResponseDeserializer( - item: any + item: any, ): _DownloadFileResponse { return { name: item["name"], file: typeof item["file"] === "string" ? stringToUint8Array(item["file"], "base64") - : item["file"] + : item["file"], }; } ``` @@ -444,12 +452,12 @@ import { StreamableMethod, PathUncheckedResponse, createRestError, - operationOptionsToRequestParameters + operationOptionsToRequestParameters, } from "@azure-rest/core-client"; export function _downloadFileSend( context: Client, - options: DownloadFileOptionalParams = { requestOptions: {} } + options: DownloadFileOptionalParams = { requestOptions: {} }, ): StreamableMethod { return context .path("/downloadFile") @@ -457,7 +465,7 @@ export function _downloadFileSend( } export async function _downloadFileDeserialize( - result: PathUncheckedResponse + result: PathUncheckedResponse, ): Promise<{ name: string; file: Uint8Array; @@ -472,7 +480,7 @@ export async function _downloadFileDeserialize( export async function downloadFile( context: Client, - options: DownloadFileOptionalParams = { requestOptions: {} } + options: DownloadFileOptionalParams = { requestOptions: {} }, ): Promise<{ name: string; file: Uint8Array; @@ -512,13 +520,13 @@ export interface _DownloadFileResponse { } export function _downloadFileResponseDeserializer( - item: any + item: any, ): _DownloadFileResponse { return { name: item["name"], file: item["file"].map((p: any) => { return typeof p === "string" ? stringToUint8Array(p, "base64") : p; - }) + }), }; } ``` @@ -532,12 +540,12 @@ import { StreamableMethod, PathUncheckedResponse, createRestError, - operationOptionsToRequestParameters + operationOptionsToRequestParameters, } from "@azure-rest/core-client"; export function _downloadFileSend( context: Client, - options: DownloadFileOptionalParams = { requestOptions: {} } + options: DownloadFileOptionalParams = { requestOptions: {} }, ): StreamableMethod { return context .path("/downloadFile") @@ -545,7 +553,7 @@ export function _downloadFileSend( } export async function _downloadFileDeserialize( - result: PathUncheckedResponse + result: PathUncheckedResponse, ): Promise<{ name: string; file: Uint8Array[]; @@ -560,7 +568,7 @@ export async function _downloadFileDeserialize( export async function downloadFile( context: Client, - options: DownloadFileOptionalParams = { requestOptions: {} } + options: DownloadFileOptionalParams = { requestOptions: {} }, ): Promise<{ name: string; file: Uint8Array[]; @@ -590,12 +598,12 @@ import { StreamableMethod, PathUncheckedResponse, createRestError, - operationOptionsToRequestParameters + operationOptionsToRequestParameters, } from "@azure-rest/core-client"; export function _testSend( context: Client, - options: TestOptionalParams = { requestOptions: {} } + options: TestOptionalParams = { requestOptions: {} }, ): StreamableMethod { return context .path("/") @@ -603,7 +611,7 @@ export function _testSend( } export async function _testDeserialize( - result: PathUncheckedResponse + result: PathUncheckedResponse, ): Promise { const expectedStatuses = ["200"]; if (!expectedStatuses.includes(result.status)) { @@ -615,7 +623,7 @@ export async function _testDeserialize( export async function test( context: Client, - options: TestOptionalParams = { requestOptions: {} } + options: TestOptionalParams = { requestOptions: {} }, ): Promise { const result = await _testSend(context, options); return _testDeserialize(result); @@ -635,7 +643,7 @@ export interface TestingClientOptionalParams extends ClientOptions {} export function createTesting( endpointParam: string, - options: TestingClientOptionalParams = {} + options: TestingClientOptionalParams = {}, ): TestingContext { const prefixFromOptions = options?.userAgentOptions?.userAgentPrefix; const userAgentPrefix = prefixFromOptions @@ -644,12 +652,12 @@ export function createTesting( const { apiVersion: _, ...updatedOptions } = { ...options, userAgentOptions: { userAgentPrefix }, - loggingOptions: { logger: options.loggingOptions?.logger ?? logger.info } + loggingOptions: { logger: options.loggingOptions?.logger ?? logger.info }, }; const clientContext = getClient( options.endpoint ?? options.baseUrl ?? String(endpointParam), undefined, - updatedOptions + updatedOptions, ); clientContext.pipeline.removePolicy({ name: "ApiVersionPolicy" }); return clientContext; @@ -670,7 +678,7 @@ export class TestingClient { constructor( endpointParam: string, - options: TestingClientOptionalParams = {} + options: TestingClientOptionalParams = {}, ) { const prefixFromOptions = options?.userAgentOptions?.userAgentPrefix; const userAgentPrefix = prefixFromOptions @@ -678,7 +686,7 @@ export class TestingClient { : `azsdk-js-client`; this._client = createTesting(endpointParam, { ...options, - userAgentOptions: { userAgentPrefix } + userAgentOptions: { userAgentPrefix }, }); this.pipeline = this._client.pipeline; } @@ -718,12 +726,12 @@ import { StreamableMethod, PathUncheckedResponse, createRestError, - operationOptionsToRequestParameters + operationOptionsToRequestParameters, } from "@azure-rest/core-client"; export function _testSend( context: Client, - options: TestOptionalParams = { requestOptions: {} } + options: TestOptionalParams = { requestOptions: {} }, ): StreamableMethod { return context .path("/") @@ -731,7 +739,7 @@ export function _testSend( } export async function _testDeserialize( - result: PathUncheckedResponse + result: PathUncheckedResponse, ): Promise { const expectedStatuses = ["200"]; if (!expectedStatuses.includes(result.status)) { @@ -743,7 +751,7 @@ export async function _testDeserialize( export async function test( context: Client, - options: TestOptionalParams = { requestOptions: {} } + options: TestOptionalParams = { requestOptions: {} }, ): Promise { const result = await _testSend(context, options); return _testDeserialize(result); @@ -765,7 +773,7 @@ export interface TestingClientOptionalParams extends ClientOptions { export function createTesting( endpointParam: string, - options: TestingClientOptionalParams = {} + options: TestingClientOptionalParams = {}, ): TestingContext { const prefixFromOptions = options?.userAgentOptions?.userAgentPrefix; const userAgentPrefix = prefixFromOptions @@ -774,12 +782,12 @@ export function createTesting( const { apiVersion: _, ...updatedOptions } = { ...options, userAgentOptions: { userAgentPrefix }, - loggingOptions: { logger: options.loggingOptions?.logger ?? logger.info } + loggingOptions: { logger: options.loggingOptions?.logger ?? logger.info }, }; const clientContext = getClient( options.endpoint ?? options.baseUrl ?? String(endpointParam), undefined, - updatedOptions + updatedOptions, ); clientContext.pipeline.removePolicy({ name: "ApiVersionPolicy" }); const apiVersion = options.apiVersion ?? "2022-05-15-preview"; @@ -796,7 +804,7 @@ export function createTesting( } return next(req); - } + }, }); return clientContext; } @@ -816,7 +824,7 @@ export class TestingClient { constructor( endpointParam: string, - options: TestingClientOptionalParams = {} + options: TestingClientOptionalParams = {}, ) { const prefixFromOptions = options?.userAgentOptions?.userAgentPrefix; const userAgentPrefix = prefixFromOptions @@ -824,7 +832,7 @@ export class TestingClient { : `azsdk-js-client`; this._client = createTesting(endpointParam, { ...options, - userAgentOptions: { userAgentPrefix } + userAgentOptions: { userAgentPrefix }, }); this.pipeline = this._client.pipeline; } @@ -854,26 +862,35 @@ op test1(): string; ```ts operations import { TestingContext as Client } from "./index.js"; +import { expandUrlTemplate } from "../static-helpers/urlTemplate.js"; import { StreamableMethod, PathUncheckedResponse, createRestError, - operationOptionsToRequestParameters + operationOptionsToRequestParameters, } from "@azure-rest/core-client"; export function _testSend( context: Client, apiVersion: string, - options: TestOptionalParams = { requestOptions: {} } + options: TestOptionalParams = { requestOptions: {} }, ): StreamableMethod { - return context.path("/test").get({ - ...operationOptionsToRequestParameters(options), - queryParameters: { "api-version": apiVersion } - }); + const path = expandUrlTemplate( + "/test{?api-version}", + { + "api-version": apiVersion, + }, + { + allowReserved: options?.requestOptions?.skipUrlEncoding, + }, + ); + return context + .path(path) + .get({ ...operationOptionsToRequestParameters(options) }); } export async function _testDeserialize( - result: PathUncheckedResponse + result: PathUncheckedResponse, ): Promise { const expectedStatuses = ["200"]; if (!expectedStatuses.includes(result.status)) { @@ -886,7 +903,7 @@ export async function _testDeserialize( export async function test( context: Client, apiVersion: string, - options: TestOptionalParams = { requestOptions: {} } + options: TestOptionalParams = { requestOptions: {} }, ): Promise { const result = await _testSend(context, apiVersion, options); return _testDeserialize(result); @@ -894,7 +911,7 @@ export async function test( export function _test1Send( context: Client, - options: Test1OptionalParams = { requestOptions: {} } + options: Test1OptionalParams = { requestOptions: {} }, ): StreamableMethod { return context .path("/test1") @@ -902,7 +919,7 @@ export function _test1Send( } export async function _test1Deserialize( - result: PathUncheckedResponse + result: PathUncheckedResponse, ): Promise { const expectedStatuses = ["200"]; if (!expectedStatuses.includes(result.status)) { @@ -914,7 +931,7 @@ export async function _test1Deserialize( export async function test1( context: Client, - options: Test1OptionalParams = { requestOptions: {} } + options: Test1OptionalParams = { requestOptions: {} }, ): Promise { const result = await _test1Send(context, options); return _test1Deserialize(result); @@ -934,7 +951,7 @@ export interface TestingClientOptionalParams extends ClientOptions {} export function createTesting( endpointParam: string, - options: TestingClientOptionalParams = {} + options: TestingClientOptionalParams = {}, ): TestingContext { const prefixFromOptions = options?.userAgentOptions?.userAgentPrefix; const userAgentPrefix = prefixFromOptions @@ -943,17 +960,17 @@ export function createTesting( const { apiVersion: _, ...updatedOptions } = { ...options, userAgentOptions: { userAgentPrefix }, - loggingOptions: { logger: options.loggingOptions?.logger ?? logger.info } + loggingOptions: { logger: options.loggingOptions?.logger ?? logger.info }, }; const clientContext = getClient( options.endpoint ?? options.baseUrl ?? String(endpointParam), undefined, - updatedOptions + updatedOptions, ); clientContext.pipeline.removePolicy({ name: "ApiVersionPolicy" }); if (options.apiVersion) { logger.warning( - "This client does not support client api-version, please change it at the operation level" + "This client does not support client api-version, please change it at the operation level", ); } return clientContext; @@ -974,7 +991,7 @@ export class TestingClient { constructor( endpointParam: string, - options: TestingClientOptionalParams = {} + options: TestingClientOptionalParams = {}, ) { const prefixFromOptions = options?.userAgentOptions?.userAgentPrefix; const userAgentPrefix = prefixFromOptions @@ -982,20 +999,20 @@ export class TestingClient { : `azsdk-js-client`; this._client = createTesting(endpointParam, { ...options, - userAgentOptions: { userAgentPrefix } + userAgentOptions: { userAgentPrefix }, }); this.pipeline = this._client.pipeline; } test( apiVersion: string, - options: TestOptionalParams = { requestOptions: {} } + options: TestOptionalParams = { requestOptions: {} }, ): Promise { return test(this._client, apiVersion, options); } test1( - options: Test1OptionalParams = { requestOptions: {} } + options: Test1OptionalParams = { requestOptions: {} }, ): Promise { return test1(this._client, options); } diff --git a/packages/typespec-ts/test/modularUnit/scenarios/clientContext/clientContext.md b/packages/typespec-ts/test/modularUnit/scenarios/clientContext/clientContext.md index 2574fe3c34..0a869ca197 100644 --- a/packages/typespec-ts/test/modularUnit/scenarios/clientContext/clientContext.md +++ b/packages/typespec-ts/test/modularUnit/scenarios/clientContext/clientContext.md @@ -71,7 +71,7 @@ export interface ServiceClientOptionalParams extends ClientOptions { export function createService( endpointParam: string, - options: ServiceClientOptionalParams = {} + options: ServiceClientOptionalParams = {}, ): ServiceContext { const clientParam = options.clientParam ?? "default"; const endpointUrl = @@ -85,13 +85,13 @@ export function createService( const { apiVersion: _, ...updatedOptions } = { ...options, userAgentOptions: { userAgentPrefix }, - loggingOptions: { logger: options.loggingOptions?.logger ?? logger.info } + loggingOptions: { logger: options.loggingOptions?.logger ?? logger.info }, }; const clientContext = getClient(endpointUrl, undefined, updatedOptions); clientContext.pipeline.removePolicy({ name: "ApiVersionPolicy" }); if (options.apiVersion) { logger.warning( - "This client does not support client api-version, please change it at the operation level" + "This client does not support client api-version, please change it at the operation level", ); } return clientContext; @@ -170,7 +170,7 @@ export interface ServiceClientOptionalParams extends ClientOptions { } export function createService( - options: ServiceClientOptionalParams = {} + options: ServiceClientOptionalParams = {}, ): ServiceContext { const endpointParam = options.endpointParam ?? "http://localhost:3000"; const clientParam = options.clientParam ?? "default"; @@ -185,13 +185,13 @@ export function createService( const { apiVersion: _, ...updatedOptions } = { ...options, userAgentOptions: { userAgentPrefix }, - loggingOptions: { logger: options.loggingOptions?.logger ?? logger.info } + loggingOptions: { logger: options.loggingOptions?.logger ?? logger.info }, }; const clientContext = getClient(endpointUrl, undefined, updatedOptions); clientContext.pipeline.removePolicy({ name: "ApiVersionPolicy" }); if (options.apiVersion) { logger.warning( - "This client does not support client api-version, please change it at the operation level" + "This client does not support client api-version, please change it at the operation level", ); } return clientContext; diff --git a/packages/typespec-ts/test/modularUnit/scenarios/enumUnion/enumUnion.md b/packages/typespec-ts/test/modularUnit/scenarios/enumUnion/enumUnion.md index 9f1e6d58eb..4955dc4777 100644 --- a/packages/typespec-ts/test/modularUnit/scenarios/enumUnion/enumUnion.md +++ b/packages/typespec-ts/test/modularUnit/scenarios/enumUnion/enumUnion.md @@ -53,24 +53,26 @@ import { StreamableMethod, PathUncheckedResponse, createRestError, - operationOptionsToRequestParameters + operationOptionsToRequestParameters, } from "@azure-rest/core-client"; export function _getSend( context: Client, contentType: SchemaContentTypeValues, body: string, - options: GetOptionalParams = { requestOptions: {} } + options: GetOptionalParams = { requestOptions: {} }, ): StreamableMethod { - return context.path("/").post({ - ...operationOptionsToRequestParameters(options), - contentType: contentType, - body: body - }); + return context + .path("/") + .post({ + ...operationOptionsToRequestParameters(options), + contentType: contentType, + body: body, + }); } export async function _getDeserialize( - result: PathUncheckedResponse + result: PathUncheckedResponse, ): Promise { const expectedStatuses = ["204"]; if (!expectedStatuses.includes(result.status)) { @@ -84,7 +86,7 @@ export async function get( context: Client, contentType: SchemaContentTypeValues, body: string, - options: GetOptionalParams = { requestOptions: {} } + options: GetOptionalParams = { requestOptions: {} }, ): Promise { const result = await _getSend(context, contentType, body, options); return _getDeserialize(result); @@ -225,7 +227,7 @@ export type SchemaContentTypeValues = | string; export function schemaContentTypeValuesSerializer( - item: SchemaContentTypeValues + item: SchemaContentTypeValues, ): any { return item; } @@ -285,7 +287,7 @@ export type SchemaContentTypeValues = | string; export function schemaContentTypeValuesSerializer( - item: SchemaContentTypeValues + item: SchemaContentTypeValues, ): any { return item; } @@ -386,7 +388,7 @@ export type SchemaContentTypeValues = | string; export function schemaContentTypeValuesSerializer( - item: SchemaContentTypeValues + item: SchemaContentTypeValues, ): any { return item; } @@ -446,7 +448,7 @@ export type SchemaContentTypeValues = | string; export function schemaContentTypeValuesSerializer( - item: SchemaContentTypeValues + item: SchemaContentTypeValues, ): any { return item; } @@ -500,24 +502,26 @@ import { StreamableMethod, PathUncheckedResponse, createRestError, - operationOptionsToRequestParameters + operationOptionsToRequestParameters, } from "@azure-rest/core-client"; export function _getSend( context: Client, testHeader: "A" | "B", body: string, - options: GetOptionalParams = { requestOptions: {} } + options: GetOptionalParams = { requestOptions: {} }, ): StreamableMethod { - return context.path("/").post({ - ...operationOptionsToRequestParameters(options), - headers: { "test-header": testHeader }, - body: body - }); + return context + .path("/") + .post({ + ...operationOptionsToRequestParameters(options), + headers: { "test-header": testHeader }, + body: body, + }); } export async function _getDeserialize( - result: PathUncheckedResponse + result: PathUncheckedResponse, ): Promise { const expectedStatuses = ["204"]; if (!expectedStatuses.includes(result.status)) { @@ -531,7 +535,7 @@ export async function get( context: Client, testHeader: "A" | "B", body: string, - options: GetOptionalParams = { requestOptions: {} } + options: GetOptionalParams = { requestOptions: {} }, ): Promise { const result = await _getSend(context, testHeader, body, options); return _getDeserialize(result); @@ -582,24 +586,26 @@ import { StreamableMethod, PathUncheckedResponse, createRestError, - operationOptionsToRequestParameters + operationOptionsToRequestParameters, } from "@azure-rest/core-client"; export function _getSend( context: Client, testHeader: string, body: string, - options: GetOptionalParams = { requestOptions: {} } + options: GetOptionalParams = { requestOptions: {} }, ): StreamableMethod { - return context.path("/").post({ - ...operationOptionsToRequestParameters(options), - headers: { "test-header": testHeader }, - body: body - }); + return context + .path("/") + .post({ + ...operationOptionsToRequestParameters(options), + headers: { "test-header": testHeader }, + body: body, + }); } export async function _getDeserialize( - result: PathUncheckedResponse + result: PathUncheckedResponse, ): Promise { const expectedStatuses = ["204"]; if (!expectedStatuses.includes(result.status)) { @@ -613,7 +619,7 @@ export async function get( context: Client, testHeader: string, body: string, - options: GetOptionalParams = { requestOptions: {} } + options: GetOptionalParams = { requestOptions: {} }, ): Promise { const result = await _getSend(context, testHeader, body, options); return _getDeserialize(result); @@ -1154,7 +1160,7 @@ export enum KnownImageSize { * A taller image size of 1792x1024 pixels. * Only supported with dall-e-3 models. */ - size1024x1792 = "1024x1792" + size1024x1792 = "1024x1792", } ``` diff --git a/packages/typespec-ts/test/modularUnit/scenarios/example/example.md b/packages/typespec-ts/test/modularUnit/scenarios/example/example.md index 7042622afc..392be91377 100644 --- a/packages/typespec-ts/test/modularUnit/scenarios/example/example.md +++ b/packages/typespec-ts/test/modularUnit/scenarios/example/example.md @@ -81,6 +81,7 @@ You can extract the entire operations file using `ts operations`: ```ts operations import { TestingContext as Client } from "./index.js"; import { Example, exampleDeserializer } from "../models/models.js"; +import { expandUrlTemplate } from "../static-helpers/urlTemplate.js"; import { StreamableMethod, PathUncheckedResponse, @@ -93,8 +94,17 @@ export function _readSend( id: string, options: ReadOptionalParams = { requestOptions: {} }, ): StreamableMethod { + const path = expandUrlTemplate( + "/{id}", + { + id: id, + }, + { + allowReserved: options?.requestOptions?.skipUrlEncoding, + }, + ); return context - .path("/{id}", id) + .path(path) .get({ ...operationOptionsToRequestParameters(options) }); } diff --git a/packages/typespec-ts/test/modularUnit/scenarios/models/serialization/errorModels.md b/packages/typespec-ts/test/modularUnit/scenarios/models/serialization/errorModels.md index 2de9734445..8b180e5c0e 100644 --- a/packages/typespec-ts/test/modularUnit/scenarios/models/serialization/errorModels.md +++ b/packages/typespec-ts/test/modularUnit/scenarios/models/serialization/errorModels.md @@ -33,7 +33,7 @@ export interface Foo { export function fooDeserializer(item: any): Foo { return { - name: item["name"] + name: item["name"], }; } ``` @@ -76,7 +76,7 @@ export interface Foo { export function fooDeserializer(item: any): Foo { return { name: item["name"], - options: errorDetailDeserializer(item["options"]) + options: errorDetailDeserializer(item["options"]), }; } @@ -88,7 +88,7 @@ export interface ErrorDetail { export function errorDetailDeserializer(item: any): ErrorDetail { return { - message: item["message"] + message: item["message"], }; } ``` diff --git a/packages/typespec-ts/test/modularUnit/scenarios/modelsGenerator/modelsGenerator.md b/packages/typespec-ts/test/modularUnit/scenarios/modelsGenerator/modelsGenerator.md index af52aff61a..31112ed7a3 100644 --- a/packages/typespec-ts/test/modularUnit/scenarios/modelsGenerator/modelsGenerator.md +++ b/packages/typespec-ts/test/modularUnit/scenarios/modelsGenerator/modelsGenerator.md @@ -64,7 +64,7 @@ export function inputOutputModelSerializer(item: InputOutputModel): any { export function inputOutputModelDeserializer(item: any): InputOutputModel { return { - prop: item["prop"] + prop: item["prop"], }; } ``` @@ -120,7 +120,7 @@ export function inputOutputModelSerializer(item: InputOutputModel): any { export function inputOutputModelDeserializer(item: any): InputOutputModel { return { - prop: item["prop"] + prop: item["prop"], }; } ``` @@ -175,7 +175,7 @@ export function inputOutputModelSerializer(item: InputOutputModel): any { export function inputOutputModelDeserializer(item: any): InputOutputModel { return { - prop: item["prop"] + prop: item["prop"], }; } ``` @@ -237,7 +237,7 @@ export function inputOutputModelSerializer(item: InputOutputModel): any { export function inputOutputModelDeserializer(item: any): InputOutputModel { return { - prop: item["prop"] + prop: item["prop"], }; } @@ -269,21 +269,23 @@ import { StreamableMethod, PathUncheckedResponse, createRestError, - operationOptionsToRequestParameters + operationOptionsToRequestParameters, } from "@azure-rest/core-client"; export function _createStreamingSend( context: Client, - options: CreateStreamingOptionalParams = { requestOptions: {} } + options: CreateStreamingOptionalParams = { requestOptions: {} }, ): StreamableMethod { - return context.path("/createStreaming").post({ - ...operationOptionsToRequestParameters(options), - body: { stream: true } - }); + return context + .path("/createStreaming") + .post({ + ...operationOptionsToRequestParameters(options), + body: { stream: true }, + }); } export async function _createStreamingDeserialize( - result: PathUncheckedResponse + result: PathUncheckedResponse, ): Promise { const expectedStatuses = ["204"]; if (!expectedStatuses.includes(result.status)) { @@ -295,7 +297,7 @@ export async function _createStreamingDeserialize( export async function createStreaming( context: Client, - options: CreateStreamingOptionalParams = { requestOptions: {} } + options: CreateStreamingOptionalParams = { requestOptions: {} }, ): Promise { const result = await _createStreamingSend(context, options); return _createStreamingDeserialize(result); @@ -336,7 +338,7 @@ export function fooSerializer(item: Foo): any { prop1: item["prop1"], prop2: item["prop2"], prop3: item["prop3"].toISOString(), - prop4: item["prop4"] + prop4: item["prop4"], }; } ``` @@ -349,7 +351,7 @@ export function fooDeserializer(item: any): Foo { prop1: item["prop1"], prop2: item["prop2"], prop3: new Date(item["prop3"]), - prop4: item["prop4"] + prop4: item["prop4"], }; } ``` @@ -363,22 +365,24 @@ import { StreamableMethod, PathUncheckedResponse, createRestError, - operationOptionsToRequestParameters + operationOptionsToRequestParameters, } from "@azure-rest/core-client"; export function _readSend( context: Client, body: Foo, - options: ReadOptionalParams = { requestOptions: {} } + options: ReadOptionalParams = { requestOptions: {} }, ): StreamableMethod { - return context.path("/").post({ - ...operationOptionsToRequestParameters(options), - body: fooSerializer(body) - }); + return context + .path("/") + .post({ + ...operationOptionsToRequestParameters(options), + body: fooSerializer(body), + }); } export async function _readDeserialize( - result: PathUncheckedResponse + result: PathUncheckedResponse, ): Promise { const expectedStatuses = ["200"]; if (!expectedStatuses.includes(result.status)) { @@ -391,7 +395,7 @@ export async function _readDeserialize( export async function read( context: Client, body: Foo, - options: ReadOptionalParams = { requestOptions: {} } + options: ReadOptionalParams = { requestOptions: {} }, ): Promise { const result = await _readSend(context, body, options); return _readDeserialize(result); @@ -414,22 +418,24 @@ import { StreamableMethod, PathUncheckedResponse, createRestError, - operationOptionsToRequestParameters + operationOptionsToRequestParameters, } from "@azure-rest/core-client"; export function _readSend( context: Client, prop: Date, - options: ReadOptionalParams = { requestOptions: {} } + options: ReadOptionalParams = { requestOptions: {} }, ): StreamableMethod { - return context.path("/").get({ - ...operationOptionsToRequestParameters(options), - headers: { prop: prop.toUTCString() } - }); + return context + .path("/") + .get({ + ...operationOptionsToRequestParameters(options), + headers: { prop: prop.toUTCString() }, + }); } export async function _readDeserialize( - result: PathUncheckedResponse + result: PathUncheckedResponse, ): Promise { const expectedStatuses = ["200"]; if (!expectedStatuses.includes(result.status)) { @@ -442,7 +448,7 @@ export async function _readDeserialize( export async function read( context: Client, prop: Date, - options: ReadOptionalParams = { requestOptions: {} } + options: ReadOptionalParams = { requestOptions: {} }, ): Promise { const result = await _readSend(context, prop, options); return _readDeserialize(result); @@ -490,22 +496,24 @@ import { StreamableMethod, PathUncheckedResponse, createRestError, - operationOptionsToRequestParameters + operationOptionsToRequestParameters, } from "@azure-rest/core-client"; export function _readSend( context: Client, body: Foo, - options: ReadOptionalParams = { requestOptions: {} } + options: ReadOptionalParams = { requestOptions: {} }, ): StreamableMethod { - return context.path("/").post({ - ...operationOptionsToRequestParameters(options), - body: fooSerializer(body) - }); + return context + .path("/") + .post({ + ...operationOptionsToRequestParameters(options), + body: fooSerializer(body), + }); } export async function _readDeserialize( - result: PathUncheckedResponse + result: PathUncheckedResponse, ): Promise { const expectedStatuses = ["200"]; if (!expectedStatuses.includes(result.status)) { @@ -518,7 +526,7 @@ export async function _readDeserialize( export async function read( context: Client, body: Foo, - options: ReadOptionalParams = { requestOptions: {} } + options: ReadOptionalParams = { requestOptions: {} }, ): Promise { const result = await _readSend(context, body, options); return _readDeserialize(result); @@ -566,22 +574,24 @@ import { StreamableMethod, PathUncheckedResponse, createRestError, - operationOptionsToRequestParameters + operationOptionsToRequestParameters, } from "@azure-rest/core-client"; export function _readSend( context: Client, body: Foo, - options: ReadOptionalParams = { requestOptions: {} } + options: ReadOptionalParams = { requestOptions: {} }, ): StreamableMethod { - return context.path("/").post({ - ...operationOptionsToRequestParameters(options), - body: fooSerializer(body) - }); + return context + .path("/") + .post({ + ...operationOptionsToRequestParameters(options), + body: fooSerializer(body), + }); } export async function _readDeserialize( - result: PathUncheckedResponse + result: PathUncheckedResponse, ): Promise { const expectedStatuses = ["200"]; if (!expectedStatuses.includes(result.status)) { @@ -594,7 +604,7 @@ export async function _readDeserialize( export async function read( context: Client, body: Foo, - options: ReadOptionalParams = { requestOptions: {} } + options: ReadOptionalParams = { requestOptions: {} }, ): Promise { const result = await _readSend(context, body, options); return _readDeserialize(result); @@ -639,22 +649,24 @@ import { StreamableMethod, PathUncheckedResponse, createRestError, - operationOptionsToRequestParameters + operationOptionsToRequestParameters, } from "@azure-rest/core-client"; export function _readSend( context: Client, body: Foo, - options: ReadOptionalParams = { requestOptions: {} } + options: ReadOptionalParams = { requestOptions: {} }, ): StreamableMethod { - return context.path("/").post({ - ...operationOptionsToRequestParameters(options), - body: fooSerializer(body) - }); + return context + .path("/") + .post({ + ...operationOptionsToRequestParameters(options), + body: fooSerializer(body), + }); } export async function _readDeserialize( - result: PathUncheckedResponse + result: PathUncheckedResponse, ): Promise { const expectedStatuses = ["200"]; if (!expectedStatuses.includes(result.status)) { @@ -667,7 +679,7 @@ export async function _readDeserialize( export async function read( context: Client, body: Foo, - options: ReadOptionalParams = { requestOptions: {} } + options: ReadOptionalParams = { requestOptions: {} }, ): Promise { const result = await _readSend(context, body, options); return _readDeserialize(result); @@ -703,22 +715,24 @@ import { StreamableMethod, PathUncheckedResponse, createRestError, - operationOptionsToRequestParameters + operationOptionsToRequestParameters, } from "@azure-rest/core-client"; export function _readSend( context: Client, body: Foo, - options: ReadOptionalParams = { requestOptions: {} } + options: ReadOptionalParams = { requestOptions: {} }, ): StreamableMethod { - return context.path("/").post({ - ...operationOptionsToRequestParameters(options), - body: fooSerializer(body) - }); + return context + .path("/") + .post({ + ...operationOptionsToRequestParameters(options), + body: fooSerializer(body), + }); } export async function _readDeserialize( - result: PathUncheckedResponse + result: PathUncheckedResponse, ): Promise { const expectedStatuses = ["200"]; if (!expectedStatuses.includes(result.status)) { @@ -731,7 +745,7 @@ export async function _readDeserialize( export async function read( context: Client, body: Foo, - options: ReadOptionalParams = { requestOptions: {} } + options: ReadOptionalParams = { requestOptions: {} }, ): Promise { const result = await _readSend(context, body, options); return _readDeserialize(result); @@ -768,22 +782,24 @@ import { StreamableMethod, PathUncheckedResponse, createRestError, - operationOptionsToRequestParameters + operationOptionsToRequestParameters, } from "@azure-rest/core-client"; export function _readSend( context: Client, body: Foo, - options: ReadOptionalParams = { requestOptions: {} } + options: ReadOptionalParams = { requestOptions: {} }, ): StreamableMethod { - return context.path("/").post({ - ...operationOptionsToRequestParameters(options), - body: fooSerializer(body) - }); + return context + .path("/") + .post({ + ...operationOptionsToRequestParameters(options), + body: fooSerializer(body), + }); } export async function _readDeserialize( - result: PathUncheckedResponse + result: PathUncheckedResponse, ): Promise { const expectedStatuses = ["200"]; if (!expectedStatuses.includes(result.status)) { @@ -796,7 +812,7 @@ export async function _readDeserialize( export async function read( context: Client, body: Foo, - options: ReadOptionalParams = { requestOptions: {} } + options: ReadOptionalParams = { requestOptions: {} }, ): Promise { const result = await _readSend(context, body, options); return _readDeserialize(result); @@ -836,22 +852,24 @@ import { StreamableMethod, PathUncheckedResponse, createRestError, - operationOptionsToRequestParameters + operationOptionsToRequestParameters, } from "@azure-rest/core-client"; export function _readSend( context: Client, body: Foo, - options: ReadOptionalParams = { requestOptions: {} } + options: ReadOptionalParams = { requestOptions: {} }, ): StreamableMethod { - return context.path("/").post({ - ...operationOptionsToRequestParameters(options), - body: fooSerializer(body) - }); + return context + .path("/") + .post({ + ...operationOptionsToRequestParameters(options), + body: fooSerializer(body), + }); } export async function _readDeserialize( - result: PathUncheckedResponse + result: PathUncheckedResponse, ): Promise { const expectedStatuses = ["200"]; if (!expectedStatuses.includes(result.status)) { @@ -864,7 +882,7 @@ export async function _readDeserialize( export async function read( context: Client, body: Foo, - options: ReadOptionalParams = { requestOptions: {} } + options: ReadOptionalParams = { requestOptions: {} }, ): Promise { const result = await _readSend(context, body, options); return _readDeserialize(result); @@ -907,7 +925,7 @@ export function fooDeserializer(item: any): Foo { prop1: typeof item["prop1"] === "string" ? stringToUint8Array(item["prop1"], "base64") - : item["prop1"] + : item["prop1"], }; } ``` @@ -921,22 +939,24 @@ import { StreamableMethod, PathUncheckedResponse, createRestError, - operationOptionsToRequestParameters + operationOptionsToRequestParameters, } from "@azure-rest/core-client"; export function _readSend( context: Client, body: Foo, - options: ReadOptionalParams = { requestOptions: {} } + options: ReadOptionalParams = { requestOptions: {} }, ): StreamableMethod { - return context.path("/").post({ - ...operationOptionsToRequestParameters(options), - body: fooSerializer(body) - }); + return context + .path("/") + .post({ + ...operationOptionsToRequestParameters(options), + body: fooSerializer(body), + }); } export async function _readDeserialize( - result: PathUncheckedResponse + result: PathUncheckedResponse, ): Promise { const expectedStatuses = ["200"]; if (!expectedStatuses.includes(result.status)) { @@ -949,7 +969,7 @@ export async function _readDeserialize( export async function read( context: Client, body: Foo, - options: ReadOptionalParams = { requestOptions: {} } + options: ReadOptionalParams = { requestOptions: {} }, ): Promise { const result = await _readSend(context, body, options); return _readDeserialize(result); @@ -993,7 +1013,7 @@ export function fooDeserializer(item: any): Foo { prop1: typeof item["prop1"] === "string" ? stringToUint8Array(item["prop1"], "base64") - : item["prop1"] + : item["prop1"], }; } ``` @@ -1007,22 +1027,24 @@ import { StreamableMethod, PathUncheckedResponse, createRestError, - operationOptionsToRequestParameters + operationOptionsToRequestParameters, } from "@azure-rest/core-client"; export function _readSend( context: Client, body: Foo, - options: ReadOptionalParams = { requestOptions: {} } + options: ReadOptionalParams = { requestOptions: {} }, ): StreamableMethod { - return context.path("/").post({ - ...operationOptionsToRequestParameters(options), - body: fooSerializer(body) - }); + return context + .path("/") + .post({ + ...operationOptionsToRequestParameters(options), + body: fooSerializer(body), + }); } export async function _readDeserialize( - result: PathUncheckedResponse + result: PathUncheckedResponse, ): Promise { const expectedStatuses = ["200"]; if (!expectedStatuses.includes(result.status)) { @@ -1035,7 +1057,7 @@ export async function _readDeserialize( export async function read( context: Client, body: Foo, - options: ReadOptionalParams = { requestOptions: {} } + options: ReadOptionalParams = { requestOptions: {} }, ): Promise { const result = await _readSend(context, body, options); return _readDeserialize(result); @@ -1079,7 +1101,7 @@ export function fooDeserializer(item: any): Foo { prop1: typeof item["prop1"] === "string" ? stringToUint8Array(item["prop1"], "base64url") - : item["prop1"] + : item["prop1"], }; } ``` @@ -1093,22 +1115,24 @@ import { StreamableMethod, PathUncheckedResponse, createRestError, - operationOptionsToRequestParameters + operationOptionsToRequestParameters, } from "@azure-rest/core-client"; export function _readSend( context: Client, body: Foo, - options: ReadOptionalParams = { requestOptions: {} } + options: ReadOptionalParams = { requestOptions: {} }, ): StreamableMethod { - return context.path("/").post({ - ...operationOptionsToRequestParameters(options), - body: fooSerializer(body) - }); + return context + .path("/") + .post({ + ...operationOptionsToRequestParameters(options), + body: fooSerializer(body), + }); } export async function _readDeserialize( - result: PathUncheckedResponse + result: PathUncheckedResponse, ): Promise { const expectedStatuses = ["200"]; if (!expectedStatuses.includes(result.status)) { @@ -1121,7 +1145,7 @@ export async function _readDeserialize( export async function read( context: Client, body: Foo, - options: ReadOptionalParams = { requestOptions: {} } + options: ReadOptionalParams = { requestOptions: {} }, ): Promise { const result = await _readSend(context, body, options); return _readDeserialize(result); @@ -1162,7 +1186,7 @@ export function catDeserializer(item: any): Cat { name: item["name"], weight: item["weight"], kind: item["kind"], - meow: item["meow"] + meow: item["meow"], }; } @@ -1175,7 +1199,7 @@ export interface Pet { export function petDeserializer(item: any): Pet { return { name: item["name"], - weight: item["weight"] + weight: item["weight"], }; } @@ -1190,7 +1214,7 @@ export function dogDeserializer(item: any): Dog { name: item["name"], weight: item["weight"], kind: item["kind"], - bark: item["bark"] + bark: item["bark"], }; } @@ -1236,7 +1260,7 @@ export function catDeserializer(item: any): Cat { name: item["name"], weight: item["weight"], kind: item["kind"], - meow: item["meow"] + meow: item["meow"], }; } @@ -1249,7 +1273,7 @@ export interface Pet { export function petDeserializer(item: any): Pet { return { name: item["name"], - weight: item["weight"] + weight: item["weight"], }; } ``` @@ -1263,12 +1287,12 @@ import { StreamableMethod, PathUncheckedResponse, createRestError, - operationOptionsToRequestParameters + operationOptionsToRequestParameters, } from "@azure-rest/core-client"; export function _readSend( context: Client, - options: ReadOptionalParams = { requestOptions: {} } + options: ReadOptionalParams = { requestOptions: {} }, ): StreamableMethod { return context .path("/") @@ -1276,7 +1300,7 @@ export function _readSend( } export async function _readDeserialize( - result: PathUncheckedResponse + result: PathUncheckedResponse, ): Promise { const expectedStatuses = ["200"]; if (!expectedStatuses.includes(result.status)) { @@ -1288,7 +1312,7 @@ export async function _readDeserialize( export async function read( context: Client, - options: ReadOptionalParams = { requestOptions: {} } + options: ReadOptionalParams = { requestOptions: {} }, ): Promise { const result = await _readSend(context, options); return _readDeserialize(result); @@ -1327,7 +1351,7 @@ export function catDeserializer(item: any): Cat { weight: item["weight"], name: item["name"], kind: item["kind"], - meow: item["meow"] + meow: item["meow"], }; } @@ -1339,7 +1363,7 @@ export interface Pet extends Animal { export function petDeserializer(item: any): Pet { return { name: item["name"], - weight: item["weight"] + weight: item["weight"], }; } @@ -1350,7 +1374,7 @@ export interface Animal { export function animalDeserializer(item: any): Animal { return { - name: item["name"] + name: item["name"], }; } ``` @@ -1364,12 +1388,12 @@ import { StreamableMethod, PathUncheckedResponse, createRestError, - operationOptionsToRequestParameters + operationOptionsToRequestParameters, } from "@azure-rest/core-client"; export function _readSend( context: Client, - options: ReadOptionalParams = { requestOptions: {} } + options: ReadOptionalParams = { requestOptions: {} }, ): StreamableMethod { return context .path("/") @@ -1377,7 +1401,7 @@ export function _readSend( } export async function _readDeserialize( - result: PathUncheckedResponse + result: PathUncheckedResponse, ): Promise { const expectedStatuses = ["200"]; if (!expectedStatuses.includes(result.status)) { @@ -1389,7 +1413,7 @@ export async function _readDeserialize( export async function read( context: Client, - options: ReadOptionalParams = { requestOptions: {} } + options: ReadOptionalParams = { requestOptions: {} }, ): Promise { const result = await _readSend(context, options); return _readDeserialize(result); @@ -1432,7 +1456,7 @@ export function catDeserializer(item: any): Cat { kind: item["kind"], name: item["name"], weight: item["weight"], - meow: item["meow"] + meow: item["meow"], }; } @@ -1447,7 +1471,7 @@ export function petDeserializer(item: any): Pet { return { kind: item["kind"], name: item["name"], - weight: item["weight"] + weight: item["weight"], }; } @@ -1474,12 +1498,12 @@ import { StreamableMethod, PathUncheckedResponse, createRestError, - operationOptionsToRequestParameters + operationOptionsToRequestParameters, } from "@azure-rest/core-client"; export function _readSend( context: Client, - options: ReadOptionalParams = { requestOptions: {} } + options: ReadOptionalParams = { requestOptions: {} }, ): StreamableMethod { return context .path("/") @@ -1487,7 +1511,7 @@ export function _readSend( } export async function _readDeserialize( - result: PathUncheckedResponse + result: PathUncheckedResponse, ): Promise { const expectedStatuses = ["200"]; if (!expectedStatuses.includes(result.status)) { @@ -1499,7 +1523,7 @@ export async function _readDeserialize( export async function read( context: Client, - options: ReadOptionalParams = { requestOptions: {} } + options: ReadOptionalParams = { requestOptions: {} }, ): Promise { const result = await _readSend(context, options); return _readDeserialize(result); @@ -1542,7 +1566,7 @@ export function petDeserializer(item: any): Pet { return { kind: item["kind"], name: item["name"], - weight: item["weight"] + weight: item["weight"], }; } @@ -1573,7 +1597,7 @@ export function catDeserializer(item: any): Cat { kind: item["kind"], name: item["name"], weight: item["weight"], - meow: item["meow"] + meow: item["meow"], }; } @@ -1588,7 +1612,7 @@ export function dogDeserializer(item: any): Dog { kind: item["kind"], name: item["name"], weight: item["weight"], - bark: item["bark"] + bark: item["bark"], }; } ``` @@ -1602,12 +1626,12 @@ import { StreamableMethod, PathUncheckedResponse, createRestError, - operationOptionsToRequestParameters + operationOptionsToRequestParameters, } from "@azure-rest/core-client"; export function _readSend( context: Client, - options: ReadOptionalParams = { requestOptions: {} } + options: ReadOptionalParams = { requestOptions: {} }, ): StreamableMethod { return context .path("/") @@ -1615,7 +1639,7 @@ export function _readSend( } export async function _readDeserialize( - result: PathUncheckedResponse + result: PathUncheckedResponse, ): Promise { const expectedStatuses = ["200"]; if (!expectedStatuses.includes(result.status)) { @@ -1627,7 +1651,7 @@ export async function _readDeserialize( export async function read( context: Client, - options: ReadOptionalParams = { requestOptions: {} } + options: ReadOptionalParams = { requestOptions: {} }, ): Promise { const result = await _readSend(context, options); return _readDeserialize(result); @@ -1676,7 +1700,7 @@ export function petDeserializer(item: any): Pet { return { kind: item["kind"], name: item["name"], - weight: item["weight"] + weight: item["weight"], }; } @@ -1707,7 +1731,7 @@ export function catDeserializer(item: any): Cat { kind: item["kind"], name: item["name"], weight: item["weight"], - meow: item["meow"] + meow: item["meow"], }; } @@ -1724,7 +1748,7 @@ export function dogDeserializer(item: any): Dog { name: item["name"], weight: item["weight"], type: item["type"], - bark: item["bark"] + bark: item["bark"], }; } @@ -1754,7 +1778,7 @@ export function goldDeserializer(item: any): Gold { bark: item["bark"], name: item["name"], weight: item["weight"], - friends: petUnionArrayDeserializer(item["friends"]) + friends: petUnionArrayDeserializer(item["friends"]), }; } @@ -1774,12 +1798,12 @@ import { StreamableMethod, PathUncheckedResponse, createRestError, - operationOptionsToRequestParameters + operationOptionsToRequestParameters, } from "@azure-rest/core-client"; export function _readSend( context: Client, - options: ReadOptionalParams = { requestOptions: {} } + options: ReadOptionalParams = { requestOptions: {} }, ): StreamableMethod { return context .path("/") @@ -1787,7 +1811,7 @@ export function _readSend( } export async function _readDeserialize( - result: PathUncheckedResponse + result: PathUncheckedResponse, ): Promise { const expectedStatuses = ["200"]; if (!expectedStatuses.includes(result.status)) { @@ -1799,7 +1823,7 @@ export async function _readDeserialize( export async function read( context: Client, - options: ReadOptionalParams = { requestOptions: {} } + options: ReadOptionalParams = { requestOptions: {} }, ): Promise { const result = await _readSend(context, options); return _readDeserialize(result); @@ -1836,7 +1860,7 @@ export function fooDeserializer(item: any): Foo { return { name: item["name"], weight: item["weight"], - bar: barDeserializer(item["bar"]) + bar: barDeserializer(item["bar"]), }; } @@ -1847,7 +1871,7 @@ export interface Bar { export function barDeserializer(item: any): Bar { return { - foo: fooDeserializer(item["foo"]) + foo: fooDeserializer(item["foo"]), }; } ``` @@ -1861,12 +1885,12 @@ import { StreamableMethod, PathUncheckedResponse, createRestError, - operationOptionsToRequestParameters + operationOptionsToRequestParameters, } from "@azure-rest/core-client"; export function _readSend( context: Client, - options: ReadOptionalParams = { requestOptions: {} } + options: ReadOptionalParams = { requestOptions: {} }, ): StreamableMethod { return context .path("/") @@ -1874,7 +1898,7 @@ export function _readSend( } export async function _readDeserialize( - result: PathUncheckedResponse + result: PathUncheckedResponse, ): Promise { const expectedStatuses = ["200"]; if (!expectedStatuses.includes(result.status)) { @@ -1886,7 +1910,7 @@ export async function _readDeserialize( export async function read( context: Client, - options: ReadOptionalParams = { requestOptions: {} } + options: ReadOptionalParams = { requestOptions: {} }, ): Promise { const result = await _readSend(context, options); return _readDeserialize(result); @@ -1950,24 +1974,26 @@ import { StreamableMethod, PathUncheckedResponse, createRestError, - operationOptionsToRequestParameters + operationOptionsToRequestParameters, } from "@azure-rest/core-client"; export function _getSend( context: Client, contentType: SchemaContentTypeValues, body: string, - options: GetOptionalParams = { requestOptions: {} } + options: GetOptionalParams = { requestOptions: {} }, ): StreamableMethod { - return context.path("/").post({ - ...operationOptionsToRequestParameters(options), - contentType: contentType, - body: body - }); + return context + .path("/") + .post({ + ...operationOptionsToRequestParameters(options), + contentType: contentType, + body: body, + }); } export async function _getDeserialize( - result: PathUncheckedResponse + result: PathUncheckedResponse, ): Promise { const expectedStatuses = ["204"]; if (!expectedStatuses.includes(result.status)) { @@ -1981,7 +2007,7 @@ export async function get( context: Client, contentType: SchemaContentTypeValues, body: string, - options: GetOptionalParams = { requestOptions: {} } + options: GetOptionalParams = { requestOptions: {} }, ): Promise { const result = await _getSend(context, contentType, body, options); return _getDeserialize(result); @@ -2077,24 +2103,26 @@ import { StreamableMethod, PathUncheckedResponse, createRestError, - operationOptionsToRequestParameters + operationOptionsToRequestParameters, } from "@azure-rest/core-client"; export function _getSend( context: Client, testHeader: "A" | "B", body: string, - options: GetOptionalParams = { requestOptions: {} } + options: GetOptionalParams = { requestOptions: {} }, ): StreamableMethod { - return context.path("/").post({ - ...operationOptionsToRequestParameters(options), - headers: { "test-header": testHeader }, - body: body - }); + return context + .path("/") + .post({ + ...operationOptionsToRequestParameters(options), + headers: { "test-header": testHeader }, + body: body, + }); } export async function _getDeserialize( - result: PathUncheckedResponse + result: PathUncheckedResponse, ): Promise { const expectedStatuses = ["204"]; if (!expectedStatuses.includes(result.status)) { @@ -2108,7 +2136,7 @@ export async function get( context: Client, testHeader: "A" | "B", body: string, - options: GetOptionalParams = { requestOptions: {} } + options: GetOptionalParams = { requestOptions: {} }, ): Promise { const result = await _getSend(context, testHeader, body, options); return _getDeserialize(result); @@ -2323,7 +2351,7 @@ export function vegetablesDeserializer(item: any): Vegetables { return { ...item, carrots: item["carrots"], - beans: item["beans"] + beans: item["beans"], }; } @@ -2331,13 +2359,13 @@ export function vegetablesDeserializer(item: any): Vegetables { export type _VegetablesAdditionalProperty = number | string; export function _vegetablesAdditionalPropertySerializer( - item: _VegetablesAdditionalProperty + item: _VegetablesAdditionalProperty, ): any { return item; } export function _vegetablesAdditionalPropertyDeserializer( - item: any + item: any, ): _VegetablesAdditionalProperty { return item; } diff --git a/packages/typespec-ts/test/modularUnit/scenarios/operations/cookieParam/ignoreCookieParam.md b/packages/typespec-ts/test/modularUnit/scenarios/operations/cookieParam/ignoreCookieParam.md index 51f2987604..3b4cd231f5 100644 --- a/packages/typespec-ts/test/modularUnit/scenarios/operations/cookieParam/ignoreCookieParam.md +++ b/packages/typespec-ts/test/modularUnit/scenarios/operations/cookieParam/ignoreCookieParam.md @@ -28,12 +28,12 @@ import { StreamableMethod, PathUncheckedResponse, createRestError, - operationOptionsToRequestParameters + operationOptionsToRequestParameters, } from "@azure-rest/core-client"; export function _testSend( context: Client, - options: TestOptionalParams = { requestOptions: {} } + options: TestOptionalParams = { requestOptions: {} }, ): StreamableMethod { return context .path("/") @@ -41,7 +41,7 @@ export function _testSend( } export async function _testDeserialize( - result: PathUncheckedResponse + result: PathUncheckedResponse, ): Promise { const expectedStatuses = ["200"]; if (!expectedStatuses.includes(result.status)) { @@ -53,7 +53,7 @@ export async function _testDeserialize( export async function test( context: Client, - options: TestOptionalParams = { requestOptions: {} } + options: TestOptionalParams = { requestOptions: {} }, ): Promise { const result = await _testSend(context, options); return _testDeserialize(result); diff --git a/packages/typespec-ts/test/modularUnit/scenarios/operations/operations.md b/packages/typespec-ts/test/modularUnit/scenarios/operations/operations.md index 5ec44d01a5..90d1660a6a 100644 --- a/packages/typespec-ts/test/modularUnit/scenarios/operations/operations.md +++ b/packages/typespec-ts/test/modularUnit/scenarios/operations/operations.md @@ -16,12 +16,12 @@ import { StreamableMethod, PathUncheckedResponse, createRestError, - operationOptionsToRequestParameters + operationOptionsToRequestParameters, } from "@azure-rest/core-client"; export function _readSend( context: Client, - options: ReadOptionalParams = { requestOptions: {} } + options: ReadOptionalParams = { requestOptions: {} }, ): StreamableMethod { return context .path("/") @@ -29,7 +29,7 @@ export function _readSend( } export async function _readDeserialize( - result: PathUncheckedResponse + result: PathUncheckedResponse, ): Promise { const expectedStatuses = ["204"]; if (!expectedStatuses.includes(result.status)) { @@ -41,7 +41,7 @@ export async function _readDeserialize( export async function read( context: Client, - options: ReadOptionalParams = { requestOptions: {} } + options: ReadOptionalParams = { requestOptions: {} }, ): Promise { const result = await _readSend(context, options); return _readDeserialize(result); @@ -64,12 +64,12 @@ import { StreamableMethod, PathUncheckedResponse, createRestError, - operationOptionsToRequestParameters + operationOptionsToRequestParameters, } from "@azure-rest/core-client"; export function _readSend( context: Client, - options: ReadOptionalParams = { requestOptions: {} } + options: ReadOptionalParams = { requestOptions: {} }, ): StreamableMethod { return context .path("/") @@ -77,7 +77,7 @@ export function _readSend( } export async function _readDeserialize( - result: PathUncheckedResponse + result: PathUncheckedResponse, ): Promise { const expectedStatuses = ["200"]; if (!expectedStatuses.includes(result.status)) { @@ -89,7 +89,7 @@ export async function _readDeserialize( export async function read( context: Client, - options: ReadOptionalParams = { requestOptions: {} } + options: ReadOptionalParams = { requestOptions: {} }, ): Promise { const result = await _readSend(context, options); return _readDeserialize(result); @@ -138,7 +138,7 @@ import { StreamableMethod, PathUncheckedResponse, createRestError, - operationOptionsToRequestParameters + operationOptionsToRequestParameters, } from "@azure-rest/core-client"; import { uint8ArrayToString } from "@azure/core-util"; @@ -151,7 +151,7 @@ export function _readSend( utcDateHeader: Date, prop1: string, prop2: number, - options: ReadOptionalParams = { requestOptions: {} } + options: ReadOptionalParams = { requestOptions: {} }, ): StreamableMethod { return context.path("/").post({ ...operationOptionsToRequestParameters(options), @@ -169,14 +169,14 @@ export function _readSend( "csv-array-header": buildCsvCollection( csvArrayHeader.map((p: any) => { return uint8ArrayToString(p, "base64url"); - }) + }), ), "utc-date-header": utcDateHeader.toUTCString(), ...(options?.optionalDateHeader !== undefined ? { "optional-date-header": !options?.optionalDateHeader ? options?.optionalDateHeader - : options?.optionalDateHeader.toUTCString() + : options?.optionalDateHeader.toUTCString(), } : {}), ...(options?.nullableDateHeader !== undefined && @@ -184,16 +184,16 @@ export function _readSend( ? { "nullable-date-header": !options?.nullableDateHeader ? options?.nullableDateHeader - : options?.nullableDateHeader.toUTCString() + : options?.nullableDateHeader.toUTCString(), } - : {}) + : {}), }, - body: { prop1: prop1, prop2: prop2 } + body: { prop1: prop1, prop2: prop2 }, }); } export async function _readDeserialize( - result: PathUncheckedResponse + result: PathUncheckedResponse, ): Promise { const expectedStatuses = ["200"]; if (!expectedStatuses.includes(result.status)) { @@ -212,7 +212,7 @@ export async function read( utcDateHeader: Date, prop1: string, prop2: number, - options: ReadOptionalParams = { requestOptions: {} } + options: ReadOptionalParams = { requestOptions: {} }, ): Promise { const result = await _readSend( context, @@ -223,7 +223,7 @@ export async function read( utcDateHeader, prop1, prop2, - options + options, ); return _readDeserialize(result); } @@ -251,22 +251,24 @@ import { StreamableMethod, PathUncheckedResponse, createRestError, - operationOptionsToRequestParameters + operationOptionsToRequestParameters, } from "@azure-rest/core-client"; export function _readSend( context: Client, nullableRequiredHeader: string | null, - options: ReadOptionalParams = { requestOptions: {} } + options: ReadOptionalParams = { requestOptions: {} }, ): StreamableMethod { - return context.path("/").get({ - ...operationOptionsToRequestParameters(options), - headers: { "nullable-required-header": nullableRequiredHeader } - }); + return context + .path("/") + .get({ + ...operationOptionsToRequestParameters(options), + headers: { "nullable-required-header": nullableRequiredHeader }, + }); } export async function _readDeserialize( - result: PathUncheckedResponse + result: PathUncheckedResponse, ): Promise { const expectedStatuses = ["200"]; if (!expectedStatuses.includes(result.status)) { @@ -279,7 +281,7 @@ export async function _readDeserialize( export async function read( context: Client, nullableRequiredHeader: string | null, - options: ReadOptionalParams = { requestOptions: {} } + options: ReadOptionalParams = { requestOptions: {} }, ): Promise { const result = await _readSend(context, nullableRequiredHeader, options); return _readDeserialize(result); @@ -307,23 +309,25 @@ import { StreamableMethod, PathUncheckedResponse, createRestError, - operationOptionsToRequestParameters + operationOptionsToRequestParameters, } from "@azure-rest/core-client"; export function _readSend( context: Client, - options: ReadOptionalParams = { requestOptions: {} } + options: ReadOptionalParams = { requestOptions: {} }, ): StreamableMethod { - return context.path("/").post({ - ...operationOptionsToRequestParameters(options), - body: !options["bars"] - ? options["bars"] - : barArraySerializer(options["bars"]) - }); + return context + .path("/") + .post({ + ...operationOptionsToRequestParameters(options), + body: !options["bars"] + ? options["bars"] + : barArraySerializer(options["bars"]), + }); } export async function _readDeserialize( - result: PathUncheckedResponse + result: PathUncheckedResponse, ): Promise { const expectedStatuses = ["200"]; if (!expectedStatuses.includes(result.status)) { @@ -335,7 +339,7 @@ export async function _readDeserialize( export async function read( context: Client, - options: ReadOptionalParams = { requestOptions: {} } + options: ReadOptionalParams = { requestOptions: {} }, ): Promise { const result = await _readSend(context, options); return _readDeserialize(result); @@ -363,22 +367,24 @@ import { StreamableMethod, PathUncheckedResponse, createRestError, - operationOptionsToRequestParameters + operationOptionsToRequestParameters, } from "@azure-rest/core-client"; export function _readSend( context: Client, bars: Bar[], - options: ReadOptionalParams = { requestOptions: {} } + options: ReadOptionalParams = { requestOptions: {} }, ): StreamableMethod { - return context.path("/").post({ - ...operationOptionsToRequestParameters(options), - body: barArraySerializer(bars) - }); + return context + .path("/") + .post({ + ...operationOptionsToRequestParameters(options), + body: barArraySerializer(bars), + }); } export async function _readDeserialize( - result: PathUncheckedResponse + result: PathUncheckedResponse, ): Promise { const expectedStatuses = ["200"]; if (!expectedStatuses.includes(result.status)) { @@ -391,7 +397,7 @@ export async function _readDeserialize( export async function read( context: Client, bars: Bar[], - options: ReadOptionalParams = { requestOptions: {} } + options: ReadOptionalParams = { requestOptions: {} }, ): Promise { const result = await _readSend(context, bars, options); return _readDeserialize(result); @@ -419,12 +425,12 @@ import { StreamableMethod, PathUncheckedResponse, createRestError, - operationOptionsToRequestParameters + operationOptionsToRequestParameters, } from "@azure-rest/core-client"; export function _readSend( context: Client, - options: ReadOptionalParams = { requestOptions: {} } + options: ReadOptionalParams = { requestOptions: {} }, ): StreamableMethod { return context .path("/") @@ -446,7 +452,7 @@ export async function _readDeserialize(result: PathUncheckedResponse): Promise< export async function read( context: Client, - options: ReadOptionalParams = { requestOptions: {} } + options: ReadOptionalParams = { requestOptions: {} }, ): Promise< { a: Bar; @@ -476,29 +482,31 @@ import { TestingContext as Client } from "./index.js"; import { Bar, barArraySerializer, - barArrayDeserializer + barArrayDeserializer, } from "../models/models.js"; import { StreamableMethod, PathUncheckedResponse, createRestError, - operationOptionsToRequestParameters + operationOptionsToRequestParameters, } from "@azure-rest/core-client"; export function _readSend( context: Client, - options: ReadOptionalParams = { requestOptions: {} } + options: ReadOptionalParams = { requestOptions: {} }, ): StreamableMethod { - return context.path("/").post({ - ...operationOptionsToRequestParameters(options), - body: !options["bars"] - ? options["bars"] - : barArraySerializer(options["bars"]) - }); + return context + .path("/") + .post({ + ...operationOptionsToRequestParameters(options), + body: !options["bars"] + ? options["bars"] + : barArraySerializer(options["bars"]), + }); } export async function _readDeserialize( - result: PathUncheckedResponse + result: PathUncheckedResponse, ): Promise { const expectedStatuses = ["200"]; if (!expectedStatuses.includes(result.status)) { @@ -510,7 +518,7 @@ export async function _readDeserialize( export async function read( context: Client, - options: ReadOptionalParams = { requestOptions: {} } + options: ReadOptionalParams = { requestOptions: {} }, ): Promise { const result = await _readSend(context, options); return _readDeserialize(result); @@ -544,22 +552,24 @@ import { StreamableMethod, PathUncheckedResponse, createRestError, - operationOptionsToRequestParameters + operationOptionsToRequestParameters, } from "@azure-rest/core-client"; export function _readSend( context: Client, body: Foo, - options: ReadOptionalParams = { requestOptions: {} } + options: ReadOptionalParams = { requestOptions: {} }, ): StreamableMethod { - return context.path("/").post({ - ...operationOptionsToRequestParameters(options), - body: fooSerializer(body) - }); + return context + .path("/") + .post({ + ...operationOptionsToRequestParameters(options), + body: fooSerializer(body), + }); } export async function _readDeserialize( - result: PathUncheckedResponse + result: PathUncheckedResponse, ): Promise { const expectedStatuses = ["200"]; if (!expectedStatuses.includes(result.status)) { @@ -572,7 +582,7 @@ export async function _readDeserialize( export async function read( context: Client, body: Foo, - options: ReadOptionalParams = { requestOptions: {} } + options: ReadOptionalParams = { requestOptions: {} }, ): Promise { const result = await _readSend(context, body, options); return _readDeserialize(result); @@ -606,12 +616,12 @@ import { StreamableMethod, PathUncheckedResponse, createRestError, - operationOptionsToRequestParameters + operationOptionsToRequestParameters, } from "@azure-rest/core-client"; export function _readSend( context: Client, - options: ReadOptionalParams = { requestOptions: {} } + options: ReadOptionalParams = { requestOptions: {} }, ): StreamableMethod { return context .path("/") @@ -619,7 +629,7 @@ export function _readSend( } export async function _readDeserialize( - result: PathUncheckedResponse + result: PathUncheckedResponse, ): Promise { const expectedStatuses = ["200"]; if (!expectedStatuses.includes(result.status)) { @@ -631,7 +641,7 @@ export async function _readDeserialize( export async function read( context: Client, - options: ReadOptionalParams = { requestOptions: {} } + options: ReadOptionalParams = { requestOptions: {} }, ): Promise { const result = await _readSend(context, options); return _readDeserialize(result); @@ -669,18 +679,18 @@ import { TestingContext as Client } from "./index.js"; import { _Bar, _barDeserializer } from "../models/models.js"; import { PagedAsyncIterableIterator, - buildPagedAsyncIterator + buildPagedAsyncIterator, } from "../static-helpers/pagingHelpers.js"; import { StreamableMethod, PathUncheckedResponse, createRestError, - operationOptionsToRequestParameters + operationOptionsToRequestParameters, } from "@azure-rest/core-client"; export function _testSend( context: Client, - options: TestOptionalParams = { requestOptions: {} } + options: TestOptionalParams = { requestOptions: {} }, ): StreamableMethod { return context .path("/") @@ -688,7 +698,7 @@ export function _testSend( } export async function _testDeserialize( - result: PathUncheckedResponse + result: PathUncheckedResponse, ): Promise<_Bar> { const expectedStatuses = ["200"]; if (!expectedStatuses.includes(result.status)) { @@ -700,14 +710,14 @@ export async function _testDeserialize( export function test( context: Client, - options: TestOptionalParams = { requestOptions: {} } + options: TestOptionalParams = { requestOptions: {} }, ): PagedAsyncIterableIterator { return buildPagedAsyncIterator( context, () => _testSend(context, options), _testDeserialize, ["200"], - { itemName: "lists" } + { itemName: "lists" }, ); } ``` @@ -745,12 +755,12 @@ import { StreamableMethod, PathUncheckedResponse, createRestError, - operationOptionsToRequestParameters + operationOptionsToRequestParameters, } from "@azure-rest/core-client"; export function _testSend( context: Client, - options: TestOptionalParams = { requestOptions: {} } + options: TestOptionalParams = { requestOptions: {} }, ): StreamableMethod { return context .path("/") @@ -758,7 +768,7 @@ export function _testSend( } export async function _testDeserialize( - result: PathUncheckedResponse + result: PathUncheckedResponse, ): Promise { const expectedStatuses = ["200"]; if (!expectedStatuses.includes(result.status)) { @@ -770,7 +780,7 @@ export async function _testDeserialize( export async function test( context: Client, - options: TestOptionalParams = { requestOptions: {} } + options: TestOptionalParams = { requestOptions: {} }, ): Promise { const result = await _testSend(context, options); return _testDeserialize(result); @@ -815,18 +825,18 @@ import { TestingContext as Client } from "./index.js"; import { _Child, _childDeserializer } from "../models/models.js"; import { PagedAsyncIterableIterator, - buildPagedAsyncIterator + buildPagedAsyncIterator, } from "../static-helpers/pagingHelpers.js"; import { StreamableMethod, PathUncheckedResponse, createRestError, - operationOptionsToRequestParameters + operationOptionsToRequestParameters, } from "@azure-rest/core-client"; export function _testSend( context: Client, - options: TestOptionalParams = { requestOptions: {} } + options: TestOptionalParams = { requestOptions: {} }, ): StreamableMethod { return context .path("/") @@ -834,7 +844,7 @@ export function _testSend( } export async function _testDeserialize( - result: PathUncheckedResponse + result: PathUncheckedResponse, ): Promise<_Child> { const expectedStatuses = ["200"]; if (!expectedStatuses.includes(result.status)) { @@ -846,14 +856,14 @@ export async function _testDeserialize( export function test( context: Client, - options: TestOptionalParams = { requestOptions: {} } + options: TestOptionalParams = { requestOptions: {} }, ): PagedAsyncIterableIterator { return buildPagedAsyncIterator( context, () => _testSend(context, options), _testDeserialize, ["200"], - { itemName: "lists", nextLinkName: "nextLink" } + { itemName: "lists", nextLinkName: "nextLink" }, ); } ``` diff --git a/packages/typespec-ts/test/modularUnit/scenarios/operations/pathParam/allowReservedFalseInAnnotation.md b/packages/typespec-ts/test/modularUnit/scenarios/operations/pathParam/allowReservedFalseInAnnotation.md index 98fac1f455..420d98a52c 100644 --- a/packages/typespec-ts/test/modularUnit/scenarios/operations/pathParam/allowReservedFalseInAnnotation.md +++ b/packages/typespec-ts/test/modularUnit/scenarios/operations/pathParam/allowReservedFalseInAnnotation.md @@ -17,6 +17,7 @@ Should normal path parameter: ```ts operations import { TestingContext as Client } from "./index.js"; +import { expandUrlTemplate } from "../static-helpers/urlTemplate.js"; import { StreamableMethod, PathUncheckedResponse, @@ -29,8 +30,17 @@ export function _annotationWithFalseSend( param: string, options: AnnotationWithFalseOptionalParams = { requestOptions: {} }, ): StreamableMethod { + const path = expandUrlTemplate( + "/annotationWithFalse/{param}", + { + param: param, + }, + { + allowReserved: options?.requestOptions?.skipUrlEncoding, + }, + ); return context - .path("/annotationWithFalse/{param}", param) + .path(path) .get({ ...operationOptionsToRequestParameters(options) }); } diff --git a/packages/typespec-ts/test/modularUnit/scenarios/operations/pathParam/allowReservedTrueInAnnotation.md b/packages/typespec-ts/test/modularUnit/scenarios/operations/pathParam/allowReservedTrueInAnnotation.md index 3579c456fd..9be93b394c 100644 --- a/packages/typespec-ts/test/modularUnit/scenarios/operations/pathParam/allowReservedTrueInAnnotation.md +++ b/packages/typespec-ts/test/modularUnit/scenarios/operations/pathParam/allowReservedTrueInAnnotation.md @@ -17,6 +17,7 @@ Should enable `allowReserved:true` for path parameter: ```ts operations import { TestingContext as Client } from "./index.js"; +import { expandUrlTemplate } from "../static-helpers/urlTemplate.js"; import { StreamableMethod, PathUncheckedResponse, @@ -29,8 +30,17 @@ export function _annotationSend( param: string, options: AnnotationOptionalParams = { requestOptions: {} }, ): StreamableMethod { + const path = expandUrlTemplate( + "/annotation/{+param}", + { + param: param, + }, + { + allowReserved: options?.requestOptions?.skipUrlEncoding, + }, + ); return context - .path("/annotation/{param}", { value: param, allowReserved: true }) + .path(path) .get({ ...operationOptionsToRequestParameters(options) }); } diff --git a/packages/typespec-ts/test/modularUnit/scenarios/operations/pathParam/allowReservedTrueWithUriTemplate.md b/packages/typespec-ts/test/modularUnit/scenarios/operations/pathParam/allowReservedTrueWithUriTemplate.md index af4531717b..2492a527ac 100644 --- a/packages/typespec-ts/test/modularUnit/scenarios/operations/pathParam/allowReservedTrueWithUriTemplate.md +++ b/packages/typespec-ts/test/modularUnit/scenarios/operations/pathParam/allowReservedTrueWithUriTemplate.md @@ -17,6 +17,7 @@ Should enable `allowReserved:true` for path parameter: ```ts operations import { TestingContext as Client } from "./index.js"; +import { expandUrlTemplate } from "../static-helpers/urlTemplate.js"; import { StreamableMethod, PathUncheckedResponse, @@ -29,8 +30,17 @@ export function _templateSend( param: string, options: TemplateOptionalParams = { requestOptions: {} }, ): StreamableMethod { + const path = expandUrlTemplate( + "/template/{+param}", + { + param: param, + }, + { + allowReserved: options?.requestOptions?.skipUrlEncoding, + }, + ); return context - .path("/template/{param}", { value: param, allowReserved: true }) + .path(path) .get({ ...operationOptionsToRequestParameters(options) }); } diff --git a/packages/typespec-ts/test/modularUnit/scenarios/operations/pathParam/allowReservedWithUriTemplate.md b/packages/typespec-ts/test/modularUnit/scenarios/operations/pathParam/allowReservedWithUriTemplate.md index af4531717b..2492a527ac 100644 --- a/packages/typespec-ts/test/modularUnit/scenarios/operations/pathParam/allowReservedWithUriTemplate.md +++ b/packages/typespec-ts/test/modularUnit/scenarios/operations/pathParam/allowReservedWithUriTemplate.md @@ -17,6 +17,7 @@ Should enable `allowReserved:true` for path parameter: ```ts operations import { TestingContext as Client } from "./index.js"; +import { expandUrlTemplate } from "../static-helpers/urlTemplate.js"; import { StreamableMethod, PathUncheckedResponse, @@ -29,8 +30,17 @@ export function _templateSend( param: string, options: TemplateOptionalParams = { requestOptions: {} }, ): StreamableMethod { + const path = expandUrlTemplate( + "/template/{+param}", + { + param: param, + }, + { + allowReserved: options?.requestOptions?.skipUrlEncoding, + }, + ); return context - .path("/template/{param}", { value: param, allowReserved: true }) + .path(path) .get({ ...operationOptionsToRequestParameters(options) }); } diff --git a/packages/typespec-ts/test/modularUnit/scenarios/operations/pathParam/requiredPathWithDefault.md b/packages/typespec-ts/test/modularUnit/scenarios/operations/pathParam/requiredPathWithDefault.md index 88ea7ef68f..7bbb4ba7e5 100644 --- a/packages/typespec-ts/test/modularUnit/scenarios/operations/pathParam/requiredPathWithDefault.md +++ b/packages/typespec-ts/test/modularUnit/scenarios/operations/pathParam/requiredPathWithDefault.md @@ -34,6 +34,7 @@ Should generate operations correctly: ```ts operations import { TestingContext as Client } from "./index.js"; +import { expandUrlTemplate } from "../static-helpers/urlTemplate.js"; import { StreamableMethod, PathUncheckedResponse, @@ -45,12 +46,18 @@ export function _readSend( context: Client, options: ReadOptionalParams = { requestOptions: {} }, ): StreamableMethod { + const path = expandUrlTemplate( + "/{strDefault}/{numberDefault}", + { + strDefault: options[strDefault] ?? "foobar", + numberDefault: options[numberDefault] ?? 1, + }, + { + allowReserved: options?.requestOptions?.skipUrlEncoding, + }, + ); return context - .path( - "/{strDefault}/{numberDefault}", - options[strDefault] ?? "foobar", - options[numberDefault] ?? 1, - ) + .path(path) .get({ ...operationOptionsToRequestParameters(options) }); } diff --git a/packages/typespec-ts/test/modularUnit/scenarios/operations/queryParam/explodeTrueWithAnnotation.md b/packages/typespec-ts/test/modularUnit/scenarios/operations/queryParam/explodeTrueWithAnnotation.md new file mode 100644 index 0000000000..3ebd0b4de1 --- /dev/null +++ b/packages/typespec-ts/test/modularUnit/scenarios/operations/queryParam/explodeTrueWithAnnotation.md @@ -0,0 +1,120 @@ +# Should generate query explode: true parameter for @query(#{ explode: true } + +## TypeSpec + +This is tsp definition. + +```tsp +model SelectQueryParameter { + @query(#{ explode: true }) + select?: string[]; +} +@route("annotation/optional") +op optional(...SelectQueryParameter): void; + +model RequiredSelectQueryParameter { + @query(#{ explode: true }) + select: string[]; +} +@route("annotation/required") +op required(...RequiredSelectQueryParameter): void; +``` + +## Provide generated operations to call rest-level methods + +## Operations + +Should enable URI template parse for parameters: + +```ts operations +import { TestingContext as Client } from "./index.js"; +import { expandUrlTemplate } from "../static-helpers/urlTemplate.js"; +import { + StreamableMethod, + PathUncheckedResponse, + createRestError, + operationOptionsToRequestParameters, +} from "@azure-rest/core-client"; + +export function _optionalSend( + context: Client, + options: OptionalOptionalParams = { requestOptions: {} }, +): StreamableMethod { + const path = expandUrlTemplate( + "/annotation/optional{?select*}", + { + select: !options?.select + ? options?.select + : options?.select.map((p: any) => { + return p; + }), + }, + { + allowReserved: options?.requestOptions?.skipUrlEncoding, + }, + ); + return context + .path(path) + .get({ ...operationOptionsToRequestParameters(options) }); +} + +export async function _optionalDeserialize( + result: PathUncheckedResponse, +): Promise { + const expectedStatuses = ["204"]; + if (!expectedStatuses.includes(result.status)) { + throw createRestError(result); + } + + return; +} + +export async function optional( + context: Client, + options: OptionalOptionalParams = { requestOptions: {} }, +): Promise { + const result = await _optionalSend(context, options); + return _optionalDeserialize(result); +} + +export function _requiredSend( + context: Client, + select: string[], + options: RequiredOptionalParams = { requestOptions: {} }, +): StreamableMethod { + const path = expandUrlTemplate( + "/annotation/required{?select*}", + { + select: select.map((p: any) => { + return p; + }), + }, + { + allowReserved: options?.requestOptions?.skipUrlEncoding, + }, + ); + return context + .path(path) + .get({ ...operationOptionsToRequestParameters(options) }); +} + +export async function _requiredDeserialize( + result: PathUncheckedResponse, +): Promise { + const expectedStatuses = ["204"]; + if (!expectedStatuses.includes(result.status)) { + throw createRestError(result); + } + + return; +} + +export async function required( + context: Client, + select: string[], + options: RequiredOptionalParams = { requestOptions: {} }, +): Promise { + const result = await _requiredSend(context, select, options); + return _requiredDeserialize(result); +} +``` diff --git a/packages/typespec-ts/test/modularUnit/scenarios/operations/queryParam/explodeTrueWithUriTemplate.md b/packages/typespec-ts/test/modularUnit/scenarios/operations/queryParam/explodeTrueWithUriTemplate.md new file mode 100644 index 0000000000..9918f1a76a --- /dev/null +++ b/packages/typespec-ts/test/modularUnit/scenarios/operations/queryParam/explodeTrueWithUriTemplate.md @@ -0,0 +1,152 @@ +# Should generate query parameter with start(\*) character + +## TypeSpec + +This is tsp definition. + +```tsp +@route("primitive?fixed=true{¶m*}") +op primitive(param: string): void; + +@route("array?fixed=true{¶m*}") +op array(param: string[]): void; + +@route("record?fixed=true{¶m*}") +op record(param: Record): void; +``` + +## Provide generated operations to call rest-level methods + +## Operations + +Should enable URI template parse for parameters: + +```ts operations +import { TestingContext as Client } from "./index.js"; +import { expandUrlTemplate } from "../static-helpers/urlTemplate.js"; +import { + StreamableMethod, + PathUncheckedResponse, + createRestError, + operationOptionsToRequestParameters, +} from "@azure-rest/core-client"; + +export function _primitiveSend( + context: Client, + param: string, + options: PrimitiveOptionalParams = { requestOptions: {} }, +): StreamableMethod { + const path = expandUrlTemplate( + "/primitive?fixed=true{¶m*}", + { + param: param, + }, + { + allowReserved: options?.requestOptions?.skipUrlEncoding, + }, + ); + return context + .path(path) + .get({ ...operationOptionsToRequestParameters(options) }); +} + +export async function _primitiveDeserialize( + result: PathUncheckedResponse, +): Promise { + const expectedStatuses = ["204"]; + if (!expectedStatuses.includes(result.status)) { + throw createRestError(result); + } + + return; +} + +export async function primitive( + context: Client, + param: string, + options: PrimitiveOptionalParams = { requestOptions: {} }, +): Promise { + const result = await _primitiveSend(context, param, options); + return _primitiveDeserialize(result); +} + +export function _arraySend( + context: Client, + param: string[], + options: ArrayOptionalParams = { requestOptions: {} }, +): StreamableMethod { + const path = expandUrlTemplate( + "/array?fixed=true{¶m*}", + { + param: param.map((p: any) => { + return p; + }), + }, + { + allowReserved: options?.requestOptions?.skipUrlEncoding, + }, + ); + return context + .path(path) + .get({ ...operationOptionsToRequestParameters(options) }); +} + +export async function _arrayDeserialize( + result: PathUncheckedResponse, +): Promise { + const expectedStatuses = ["204"]; + if (!expectedStatuses.includes(result.status)) { + throw createRestError(result); + } + + return; +} + +export async function array( + context: Client, + param: string[], + options: ArrayOptionalParams = { requestOptions: {} }, +): Promise { + const result = await _arraySend(context, param, options); + return _arrayDeserialize(result); +} + +export function _recordSend( + context: Client, + param: Record, + options: RecordOptionalParams = { requestOptions: {} }, +): StreamableMethod { + const path = expandUrlTemplate( + "/record?fixed=true{¶m*}", + { + param: param, + }, + { + allowReserved: options?.requestOptions?.skipUrlEncoding, + }, + ); + return context + .path(path) + .get({ ...operationOptionsToRequestParameters(options) }); +} + +export async function _recordDeserialize( + result: PathUncheckedResponse, +): Promise { + const expectedStatuses = ["204"]; + if (!expectedStatuses.includes(result.status)) { + throw createRestError(result); + } + + return; +} + +export async function record( + context: Client, + param: Record, + options: RecordOptionalParams = { requestOptions: {} }, +): Promise { + const result = await _recordSend(context, param, options); + return _recordDeserialize(result); +} +``` diff --git a/packages/typespec-ts/test/modularUnit/scenarios/samples/parameters/bodyOptionalCheck.md b/packages/typespec-ts/test/modularUnit/scenarios/samples/parameters/bodyOptionalCheck.md index 2d5b191fa7..3d2b7bb556 100644 --- a/packages/typespec-ts/test/modularUnit/scenarios/samples/parameters/bodyOptionalCheck.md +++ b/packages/typespec-ts/test/modularUnit/scenarios/samples/parameters/bodyOptionalCheck.md @@ -79,6 +79,7 @@ import { bodyParameterSerializer, _readResponseDeserializer, } from "../models/models.js"; +import { expandUrlTemplate } from "../static-helpers/urlTemplate.js"; import { StreamableMethod, PathUncheckedResponse, @@ -92,14 +93,21 @@ export function _readSend( requiredQuery: string, options: ReadOptionalParams = { requestOptions: {} }, ): StreamableMethod { + const path = expandUrlTemplate( + "/{name}{?requiredQuery,optionalQuery}", + { + name: name, + requiredQuery: requiredQuery, + optionalQuery: options?.optionalQuery, + }, + { + allowReserved: options?.requestOptions?.skipUrlEncoding, + }, + ); return context - .path("/{name}", name) + .path(path) .post({ ...operationOptionsToRequestParameters(options), - queryParameters: { - requiredQuery: requiredQuery, - optionalQuery: options?.optionalQuery, - }, body: !options["widget"] ? options["widget"] : bodyParameterSerializer(options["widget"]), diff --git a/packages/typespec-ts/test/modularUnit/static/urlTemplate.spec.ts b/packages/typespec-ts/test/modularUnit/static/urlTemplate.spec.ts new file mode 100644 index 0000000000..cde374089e --- /dev/null +++ b/packages/typespec-ts/test/modularUnit/static/urlTemplate.spec.ts @@ -0,0 +1,426 @@ +import { assert } from "chai"; +import { expandUrlTemplate, UrlTemplateOptions } from "../../../static/static-helpers/urlTemplate.js"; + +function createAssertion(context: Record, option?: UrlTemplateOptions) { + return (template: string, expected: string) => { + assert.equal(expandUrlTemplate(template, context, option), expected); + }; +} +describe("url-template", () => { + describe("Cases from RFC Spec Examples", () => { + describe("Level 1", () => { + const assert = createAssertion({ + 'var': 'value', + 'some.value': 'some', + 'some_value': 'value', + 'Some%20Thing': 'hello', + 'foo': 'bar', + 'hello': 'Hello World!', + 'bool': false, + 'toString': 'string', + 'number': 42, + 'float': 3.14, + 'undef': undefined, + 'null': null, + 'chars': 'šö䟜ñꀣ¥‡ÑÒÓÔÕÖ×ØÙÚàáâãäåæçÿü', + 'chinese': '中文', + 'surrogatepairs': '\uD834\uDF06' + }); + it("should be empty string", () => { + assert('', ''); + }); + it("should be plain url", () => { + assert('hello/world/five#5', 'hello/world/five#5'); + }); + it("should encodes non-alphanumeric chars", () => { + assert(':/?#@!$&()*+,;=\'', ':/?#@!$&()*+,;=\''); + }); + it("should encode [] correctly", () => { + assert('[0-9]:[1-8]', '%5B0-9%5D:%5B1-8%5D'); + }); + it("should not double encoded values", () => { + assert('%20', '%20'); + assert('%xyz', '%25xyz'); + assert('%', '%25'); + }); + it("should encode space by default", () => { + assert('Hello World!/{foo}', 'Hello%20World!/bar'); + }); + it('should expand plain ASCII strings', () => { + assert('{var}', 'value'); + }); + + it('should expand non-ASCII strings', () => { + assert('{chars}', '%C5%A1%C3%B6%C3%A4%C5%B8%C5%93%C3%B1%C3%AA%E2%82%AC%C2%A3%C2%A5%E2%80%A1%C3%91%C3%92%C3%93%C3%94%C3%95%C3%96%C3%97%C3%98%C3%99%C3%9A%C3%A0%C3%A1%C3%A2%C3%A3%C3%A4%C3%A5%C3%A6%C3%A7%C3%BF%C3%BC'); + assert('{chinese}', '%E4%B8%AD%E6%96%87'); + }); + + it('should expand and encode surrogate pairs correctly', () => { + assert('{surrogatepairs}', '%F0%9D%8C%86'); + }); + + it('should expand expressions with dot and underscore', () => { + assert('{some.value}', 'some'); + assert('{some_value}', 'value'); + }); + + it('should expand expressions with encoding', () => { + assert('{Some%20Thing}', 'hello'); + }); + + it('should expand expressions with reserved JavaScript names', () => { + assert('{toString}', 'string'); + }); + + it('should expand variables that are not strings', () => { + assert('{number}', '42'); + assert('{float}', '3.14'); + assert('{bool}', 'false'); + }); + + it('should expand variables that are undefined', () => { + assert('{undef}', ''); + }); + it('should expand variables that are null', () => { + assert('{null}', ''); + }); + it('should expand multiple values', () => { + assert('{var}/{foo}', 'value/bar'); + }); + it('should escape invalid characters correctly', () => { + assert('{hello}', 'Hello%20World%21'); + }); + }); + + describe('Level 2', () => { + const assert = createAssertion({ + 'var': 'value', + 'hello': 'Hello World!', + 'path': '/foo/bar' + }); + + it('reserved expansion of basic strings', () => { + assert('{+var}', 'value'); + assert('{+hello}', 'Hello%20World!'); + }); + + it('preserves paths', () => { + assert('{+path}/here', '/foo/bar/here'); + assert('here?ref={+path}', 'here?ref=/foo/bar'); + }); + }); + + describe('Level 3', () => { + const assert = createAssertion({ + 'var': 'value', + 'hello': 'Hello World!', + 'empty': '', + 'path': '/foo/bar', + 'x': '1024', + 'y': '768' + }); + + it('variables without an operator', () => { + assert('map?{x,y}', 'map?1024,768'); + }); + + it('should encode !', () => { + assert('{x,hello,y}', '1024,Hello%20World%21,768'); + assert('{#x,hello,y}', '#1024,Hello%20World!,768'); + }); + + it('variables with the reserved expansion operator', () => { + assert('{+path,x}/here', '/foo/bar,1024/here'); + assert('{+x,hello,y}', '1024,Hello%20World!,768'); + }); + + it('variables with the fragment expansion operator', () => { + assert('{#path,x}/here', '#/foo/bar,1024/here'); + }); + + it('variables with the dot operator', () => { + assert('X{.var}', 'X.value'); + assert('X{.x,y}', 'X.1024.768'); + }); + + it('variables with the path operator', () => { + assert('{/var}', '/value'); + assert('{/var,x}/here', '/value/1024/here'); + }); + + it('variables with the parameter operator', () => { + assert('{;x,y}', ';x=1024;y=768'); + assert('{;x,y,empty}', ';x=1024;y=768;empty'); + }); + + it('variables with the query operator', () => { + assert('{?x,y}', '?x=1024&y=768'); + assert('{?x,y,empty}', '?x=1024&y=768&empty='); + }); + + it('variables with the query continuation operator', () => { + assert('?fixed=yes{&x}', '?fixed=yes&x=1024'); + assert('{&x,y,empty}', '&x=1024&y=768&empty='); + }); + }); + + describe('Level 4', () => { + const assert = createAssertion({ + 'var': 'value', + 'hello': 'Hello World!', + 'path': '/foo/bar', + 'list': ['red', 'green', 'blue'], + 'keys': { + 'semi': ';', + 'dot': '.', + 'comma': ',' + }, + "chars": { + 'ü': 'ü' + }, + 'number': 2133, + 'emptystring': '', + 'emptylist': [], + 'emptyobject': {}, + 'undefinedlistitem': [1, , 2], + 'undefinedobjectitem': { key: null, hello: 'world', 'empty': '', '': 'nothing' } + }); + + it('variable empty list', () => { + assert('{/emptylist}', ''); + assert('{/emptylist*}', ''); + assert('{?emptylist*}', ''); + assert('{?emptylist}', '?emptylist='); + }); + + it('variable empty object', () => { + assert('{/emptyobject}', ''); + assert('{/emptyobject*}', ''); + assert('{?emptyobject*}', ''); + assert('{?emptyobject}', '?emptyobject='); + }); + + it('variable undefined list item', () => { + assert('{undefinedlistitem}', '1,2'); + assert('{undefinedlistitem*}', '1,2'); + assert('{?undefinedlistitem*}', '?undefinedlistitem=1&undefinedlistitem=2'); + }); + + it('variable undefined object item', () => { + assert('{undefinedobjectitem}', 'hello,world,empty,,,nothing'); + assert('{undefinedobjectitem*}', 'hello=world,empty=,nothing'); + }); + + it('variable empty string', () => { + assert('{emptystring}', ''); + assert('{+emptystring}', ''); + assert('{#emptystring}', '#'); + assert('{.emptystring}', '.'); + assert('{/emptystring}', '/'); + assert('{;emptystring}', ';emptystring'); + assert('{?emptystring}', '?emptystring='); + assert('{&emptystring}', '&emptystring='); + }); + + it('variable modifiers prefix', () => { + assert('{var:3}', 'val'); + assert('{var:30}', 'value'); + assert('{+path:6}/here', '/foo/b/here'); + assert('{#path:6}/here', '#/foo/b/here'); + assert('X{.var:3}', 'X.val'); + assert('{/var:1,var}', '/v/value'); + assert('{;hello:5}', ';hello=Hello'); + assert('{?var:3}', '?var=val'); + assert('{&var:3}', '&var=val'); + }); + + it('variable modifier prefix converted to string', () => { + assert('{number:3}', '213'); + assert('{/list*,path:4}', '/red/green/blue/%2Ffoo'); + }); + + it('variable list expansion', () => { + assert('{list}', 'red,green,blue'); + assert('{+list}', 'red,green,blue'); + assert('{#list}', '#red,green,blue'); + assert('{/list}', '/red,green,blue'); + assert('{;list}', ';list=red,green,blue'); + assert('{.list}', '.red,green,blue'); + assert('{?list}', '?list=red,green,blue'); + assert('{&list}', '&list=red,green,blue'); + }); + + it('variable associative array expansion', () => { + assert('{keys}', 'semi,%3B,dot,.,comma,%2C'); + assert('{keys*}', 'semi=%3B,dot=.,comma=%2C'); + assert('{+keys}', 'semi,;,dot,.,comma,,'); + assert('{#keys}', '#semi,;,dot,.,comma,,'); + assert('{.keys}', '.semi,%3B,dot,.,comma,%2C'); + assert('{/keys}', '/semi,%3B,dot,.,comma,%2C'); + assert('{;keys}', ';keys=semi,%3B,dot,.,comma,%2C'); + assert('{?keys}', '?keys=semi,%3B,dot,.,comma,%2C'); + assert('{&keys}', '&keys=semi,%3B,dot,.,comma,%2C'); + }); + + it('variable list explode', () => { + assert('{list*}', 'red,green,blue'); + assert('{+list*}', 'red,green,blue'); + assert('{#list*}', '#red,green,blue'); + assert('{/list*}', '/red/green/blue'); + assert('{;list*}', ';list=red;list=green;list=blue'); + assert('{.list*}', '.red.green.blue'); + assert('{?list*}', '?list=red&list=green&list=blue'); + assert('{&list*}', '&list=red&list=green&list=blue'); + }); + + it('variable associative array explode', () => { + assert('{+keys*}', 'semi=;,dot=.,comma=,'); + assert('{#keys*}', '#semi=;,dot=.,comma=,'); + assert('{/keys*}', '/semi=%3B/dot=./comma=%2C'); + assert('{;keys*}', ';semi=%3B;dot=.;comma=%2C'); + assert('{?keys*}', '?semi=%3B&dot=.&comma=%2C'); + assert('{&keys*}', '&semi=%3B&dot=.&comma=%2C') + }); + + it('encodes associative arrays correctly', () => { + assert('{chars*}', '%C3%BC=%C3%BC'); + }); + }); + }); + + + describe('apiVersion', () => { + it('should expand apiVersion', () => { + const assert = createAssertion({ + 'api-version': '2023-05-01.17.0', + 'timeOut': undefined + }); + assert('/pools{?api-version,timeOut}', '/pools?api-version=2023-05-01.17.0'); + }); + }); + + describe("allowReserved option", () => { + it("should not encode reserved characters if enable allowReserved", () => { + const assert = createAssertion({ + 'path': '/foo/bar', + 'query': 'bar,baz' + }, { allowReserved: true }); + assert('{path}/here{?query}', '/foo/bar/here?query=bar,baz'); + }); + + it("should encode reserved characters if disable allowReserved", () => { + const assert = createAssertion({ + 'path': '/foo/bar', + 'query': 'bar,baz' + }, { allowReserved: false }); + assert('{path}/here{?query}', '%2Ffoo%2Fbar/here?query=bar%2Cbaz'); + }); + + }); + + describe("path parameters", () => { + describe("simple expansion", () => { + it("should resolve primitive parameter", () => { + const assert = createAssertion({ + 'param': 'a', + }); + assert('/primitive{param}', '/primitivea'); + }); + it("should resolve array parameter", () => { + const assert = createAssertion({ + 'param': ["a", "b"], + }); + assert('/array{param}', '/arraya,b'); + }); + + it("should resolve object parameter", () => { + const assert = createAssertion({ + 'param': { a: 1, b: 2 }, + }); + assert('/record{param}', '/recorda,1,b,2'); + }); + }); + + describe("simple expansion with explode modifier*", () => { + it("should resolve primitive parameter", () => { + const assert = createAssertion({ + 'param': 'a', + }); + assert('/primitive{param*}', '/primitivea'); + }); + it("should resolve array parameter", () => { + const assert = createAssertion({ + 'param': ["a", "b"], + }); + assert('/array{param*}', '/arraya,b'); + }); + + it("should resolve object parameter", () => { + const assert = createAssertion({ + 'param': { a: 1, b: 2 }, + }); + assert('/record{param*}', '/recorda=1,b=2'); + }); + }); + + describe("path expansion", () => { + it("should resolve primitive parameter", () => { + const assert = createAssertion({ + 'param': 'a', + }); + assert('/primitive{/param}', "/primitive/a"); + }); + it("should resolve array parameter", () => { + const assert = createAssertion({ + 'param': ["a", "b"], + }); + assert('/array{/param}', '/array/a,b'); + }); + + it("should resolve object parameter", () => { + const assert = createAssertion({ + 'param': { a: 1, b: 2 }, + }); + assert('/record{/param}', '/record/a,1,b,2'); + }); + }); + + describe("path expansion with explode modifier*", () => { + it("should resolve primitive parameter", () => { + const assert = createAssertion({ + 'param': 'a', + }); + assert('/primitive{/param*}', "/primitive/a"); + }); + it("should resolve array parameter", () => { + const assert = createAssertion({ + 'param': ["a", "b"], + }); + assert('/array{/param*}', '/array/a/b'); + }); + + it("should resolve object parameter", () => { + const assert = createAssertion({ + 'param': { a: 1, b: 2 }, + }); + assert('/record{/param*}', '/record/a=1/b=2'); + }); + }); + }); + + describe("Advanced cases", () => { + it("should not double encode values", () => { + const assert = createAssertion({ + 'bar': 'bar/hello%20world', + }); + assert('/foo/{+bar}', '/foo/bar/hello%20world'); + }); + it("should encode spaces", () => { + const assert = createAssertion({ + 'bar': 'bar/hello world', + }); + assert('/foo/{+bar}', '/foo/bar/hello%20world'); + + }); + }); +}); diff --git a/packages/typespec-ts/test/util/testUtil.ts b/packages/typespec-ts/test/util/testUtil.ts index 671bde3f35..eb5894f767 100644 --- a/packages/typespec-ts/test/util/testUtil.ts +++ b/packages/typespec-ts/test/util/testUtil.ts @@ -24,7 +24,8 @@ import { getDirname } from "../../src/utils/dirname.js"; import { PagingHelpers, PollingHelpers, - SerializationHelpers + SerializationHelpers, + UrlTemplateHelpers } from "../../src/modular/static-helpers-metadata.js"; import { AzureCoreDependencies, @@ -264,7 +265,8 @@ export async function provideBinderWithAzureDependencies(project: Project) { const staticHelpers = { ...SerializationHelpers, ...PagingHelpers, - ...PollingHelpers + ...PollingHelpers, + ...UrlTemplateHelpers }; const staticHelperMap = await loadStaticHelpers(project, staticHelpers, { diff --git a/packages/typespec-ts/tsconfig.test.json b/packages/typespec-ts/tsconfig.test.json index b0fcd9a65b..cd253a3443 100644 --- a/packages/typespec-ts/tsconfig.test.json +++ b/packages/typespec-ts/tsconfig.test.json @@ -1,5 +1,6 @@ { "extends": "./tsconfig.json", + "include": ["src/**/*.ts", "static/**/*.ts"], "compilerOptions": { "module": "node16", "moduleResolution": "node16"