diff --git a/.vscode/launch.json b/.vscode/launch.json index 1f6a0fe332..f66325949e 100755 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -123,11 +123,7 @@ "name": "[TypeSpec] Debug generate integration code", "request": "launch", "cwd": "${workspaceFolder}/packages/typespec-ts", - "runtimeArgs": [ - "node", - "./test/commands/gen-cadl-ranch.js", - "--tag=modular" - ], + "runtimeArgs": ["tsx", "./test/commands/gen-cadl-ranch.js", "--tag=rlc"], "runtimeExecutable": "npx", "skipFiles": ["/**"], "type": "pwa-node", diff --git a/packages/rlc-common/src/buildClientDefinitions.ts b/packages/rlc-common/src/buildClientDefinitions.ts index 1ca2cfcf58..17f0db9433 100644 --- a/packages/rlc-common/src/buildClientDefinitions.ts +++ b/packages/rlc-common/src/buildClientDefinitions.ts @@ -14,6 +14,7 @@ import * as path from "path"; import { buildMethodDefinitions, + getGeneratedWrapperTypes, getPathParamDefinitions } from "./helpers/operationHelpers.js"; import { PathMetadata, Paths, RLCModel } from "./interfaces.js"; @@ -168,6 +169,9 @@ function getPathFirstRoutesInterfaceDefinition( sourcefile ); const pathParams = paths[key].pathParameters; + getGeneratedWrapperTypes(pathParams).forEach((p) => + options.importedParameters.add(p.name ?? p.type) + ); signatures.push({ docs: [ `Resource for '${key diff --git a/packages/rlc-common/src/buildObjectTypes.ts b/packages/rlc-common/src/buildObjectTypes.ts index 474359c964..bdfc591b1a 100644 --- a/packages/rlc-common/src/buildObjectTypes.ts +++ b/packages/rlc-common/src/buildObjectTypes.ts @@ -283,7 +283,7 @@ function getPolymorphicTypeAlias( * Builds the interface for the current object schema. If it is a polymorphic * root node it will suffix it with Base. */ -function getObjectInterfaceDeclaration( +export function getObjectInterfaceDeclaration( model: RLCModel, baseName: string, objectSchema: ObjectSchema, diff --git a/packages/rlc-common/src/buildParameterTypes.ts b/packages/rlc-common/src/buildParameterTypes.ts index 09048ac406..8f07031639 100644 --- a/packages/rlc-common/src/buildParameterTypes.ts +++ b/packages/rlc-common/src/buildParameterTypes.ts @@ -14,7 +14,8 @@ import { ParameterMetadata, ParameterMetadatas, RLCModel, - Schema + Schema, + SchemaContext } from "./interfaces.js"; import { getImportModuleName, @@ -22,6 +23,8 @@ import { getParameterTypeName } from "./helpers/nameConstructors.js"; import { getImportSpecifier } from "./helpers/importsUtil.js"; +import { getObjectInterfaceDeclaration } from "./buildObjectTypes.js"; +import { getGeneratedWrapperTypes } from "./helpers/operationHelpers.js"; export function buildParameterTypes(model: RLCModel) { const project = new Project(); @@ -58,12 +61,14 @@ export function buildParameterTypes(model: RLCModel) { ? `${baseParameterName}RequestParameters${nameSuffix}` : topParamName; const queryParameterDefinitions = buildQueryParameterDefinition( + model, parameter, baseParameterName, internalReferences, i ); const pathParameterDefinitions = buildPathParameterDefinitions( + model, parameter, baseParameterName, parametersFile, @@ -103,7 +108,7 @@ export function buildParameterTypes(model: RLCModel) { parametersFile.addInterfaces([ ...(bodyParameterDefinition ?? []), ...(queryParameterDefinitions ?? []), - ...(pathParameterDefinitions ? [pathParameterDefinitions] : []), + ...(pathParameterDefinitions ?? []), ...(headerParameterDefinitions ? [headerParameterDefinitions] : []), ...(contentTypeParameterDefinition ? [contentTypeParameterDefinition] @@ -177,6 +182,7 @@ export function buildParameterTypes(model: RLCModel) { } function buildQueryParameterDefinition( + model: RLCModel, parameters: ParameterMetadatas, baseName: string, internalReferences: Set, @@ -198,6 +204,18 @@ function buildQueryParameterDefinition( const propertiesDefinition = queryParameters.map((qp) => getPropertyFromSchema(qp.param) ); + // Get wrapper types for query parameters + const wrapperTypesDefinition = getGeneratedWrapperTypes(queryParameters).map( + (wrapObj) => { + return getObjectInterfaceDeclaration( + model, + wrapObj.name, + wrapObj, + [SchemaContext.Input], + new Set() + ); + } + ); const hasRequiredParameters = propertiesDefinition.some( (p) => !p.hasQuestionToken @@ -227,7 +245,7 @@ function buildQueryParameterDefinition( // Mark the queryParameter interface for importing internalReferences.add(queryParameterInterfaceName); - return [propertiesInterface, parameterInterface]; + return [...wrapperTypesDefinition, propertiesInterface, parameterInterface]; } function getPropertyFromSchema(schema: Schema): PropertySignatureStructure { @@ -242,42 +260,79 @@ function getPropertyFromSchema(schema: Schema): PropertySignatureStructure { } function buildPathParameterDefinitions( + model: RLCModel, parameters: ParameterMetadatas, baseName: string, parametersFile: SourceFile, internalReferences: Set, requestIndex: number -): InterfaceDeclarationStructure | undefined { +): InterfaceDeclarationStructure[] | undefined { const pathParameters = (parameters.parameters || []).filter( (p) => p.type === "path" ); if (!pathParameters.length) { return undefined; } + const allDefinitions: InterfaceDeclarationStructure[] = []; + + buildClientPathParameters(); + buildMethodWrapParameters(); + return allDefinitions; + function buildClientPathParameters() { + // we only have client-level path parameters if the source is from swagger + if (model.options?.sourceFrom === "TypeSpec") { + return; + } + const clientPathParams = pathParameters.length > 0 ? pathParameters : []; + const nameSuffix = requestIndex > 0 ? `${requestIndex}` : ""; + const pathParameterInterfaceName = `${baseName}PathParam${nameSuffix}`; - const nameSuffix = requestIndex > 0 ? `${requestIndex}` : ""; - const pathParameterInterfaceName = `${baseName}PathParam${nameSuffix}`; + const pathInterface = getPathInterfaceDefinition( + clientPathParams, + baseName + ); - const pathInterface = getPathInterfaceDefinition(pathParameters, baseName); + if (pathInterface) { + parametersFile.addInterface(pathInterface); + } - if (pathInterface) { - parametersFile.addInterface(pathInterface); - } + internalReferences.add(pathParameterInterfaceName); - internalReferences.add(pathParameterInterfaceName); + allDefinitions.push({ + isExported: true, + kind: StructureKind.Interface, + name: pathParameterInterfaceName, + properties: [ + { + name: "pathParameters", + type: `${baseName}PathParameters`, + kind: StructureKind.PropertySignature + } + ] + }); + } - return { - isExported: true, - kind: StructureKind.Interface, - name: pathParameterInterfaceName, - properties: [ - { - name: "pathParameters", - type: `${baseName}PathParameters`, - kind: StructureKind.PropertySignature - } - ] - }; + function buildMethodWrapParameters() { + if (model.options?.sourceFrom === "Swagger") { + return; + } + // we only have method-level path parameters if the source is from typespec + const methodPathParams = pathParameters.length > 0 ? pathParameters : []; + + // we only need to build the wrapper types if the path parameters are objects + const wrapperTypesDefinition = getGeneratedWrapperTypes( + methodPathParams + ).map((wrap) => { + return getObjectInterfaceDeclaration( + model, + wrap.name, + wrap, + [SchemaContext.Input], + new Set() + ); + }); + allDefinitions.push(...wrapperTypesDefinition); + } } function getPathInterfaceDefinition( diff --git a/packages/rlc-common/src/helpers/operationHelpers.ts b/packages/rlc-common/src/helpers/operationHelpers.ts index 3de0af2e56..06955b38b8 100644 --- a/packages/rlc-common/src/helpers/operationHelpers.ts +++ b/packages/rlc-common/src/helpers/operationHelpers.ts @@ -9,8 +9,10 @@ import { import { Methods, ObjectSchema, + ParameterMetadata, PathParameter, RLCModel, + Schema, SchemaContext } from "../interfaces.js"; import { NameType, normalizeName, pascalCase } from "./nameUtils.js"; @@ -121,3 +123,27 @@ function hasSchemaContextObject(model: RLCModel, schemaUsage: SchemaContext[]) { return objectSchemas.length > 0; } + +export function getGeneratedWrapperTypes( + params: ParameterMetadata[] | PathParameter[] +): Schema[] { + const wrapperTypes = params + .map((qp) => + isParameterMetadata(qp) ? qp.param.wrapperType : qp.wrapperType + ) + .filter((v) => v !== undefined); + const wrapperFromObjects = wrapperTypes.filter( + (wrap) => wrap.type === "object" + ); + const wrapperFromUnions = wrapperTypes + .filter((wrap) => wrap.type === "union") + .flatMap((wrapperType) => wrapperType?.enum ?? []) + .filter((v) => v.type === "object"); + return [...wrapperFromUnions, ...wrapperFromObjects]; +} + +function isParameterMetadata( + param: ParameterMetadata | PathParameter +): param is ParameterMetadata { + return (param as any).param !== undefined; +} diff --git a/packages/rlc-common/src/interfaces.ts b/packages/rlc-common/src/interfaces.ts index f8468959aa..374b5b93c0 100644 --- a/packages/rlc-common/src/interfaces.ts +++ b/packages/rlc-common/src/interfaces.ts @@ -176,6 +176,7 @@ export type PathParameter = { type: string; description?: string; value?: string | number | boolean; + wrapperType?: Schema; }; export interface OperationHelperDetail { @@ -249,6 +250,7 @@ export interface RLCOptions { experimentalExtensibleEnums?: boolean; clearOutputFolder?: boolean; ignorePropertyNameNormalize?: boolean; + compatibilityQueryMultiFormat?: boolean; } export interface ServiceInfo { @@ -361,7 +363,12 @@ export interface ParameterBodySchema extends Schema { export interface ParameterMetadata { type: "query" | "path" | "header"; name: string; - param: Schema; + param: ParameterSchema; +} + +export interface ParameterSchema extends Schema { + // the detailed wrapper type for the parameter and codegen needs to build this type directly + wrapperType?: Schema; } export interface OperationResponse { diff --git a/packages/typespec-test/test/faceai/generated/typespec-ts/review/ai-face-rest.api.md b/packages/typespec-test/test/faceai/generated/typespec-ts/review/ai-face-rest.api.md index 12eaf35c8f..c0f70c23de 100644 --- a/packages/typespec-test/test/faceai/generated/typespec-ts/review/ai-face-rest.api.md +++ b/packages/typespec-test/test/faceai/generated/typespec-ts/review/ai-face-rest.api.md @@ -103,10 +103,17 @@ export interface AddFaceListFaceFromUrlQueryParam { // @public (undocumented) export interface AddFaceListFaceFromUrlQueryParamProperties { detectionModel?: DetectionModel; - targetFace?: number[]; + targetFace?: number[] | AddFaceListFaceFromUrlTargetFaceQueryParam; userData?: string; } +// @public +export interface AddFaceListFaceFromUrlTargetFaceQueryParam { + explode: false; + style: "form"; + value: number[]; +} + // @public (undocumented) export interface AddFaceListFaceMediaTypesParam { contentType: "application/octet-stream"; @@ -124,10 +131,17 @@ export interface AddFaceListFaceQueryParam { // @public (undocumented) export interface AddFaceListFaceQueryParamProperties { detectionModel?: DetectionModel; - targetFace?: number[]; + targetFace?: number[] | AddFaceListFaceTargetFaceQueryParam; userData?: string; } +// @public +export interface AddFaceListFaceTargetFaceQueryParam { + explode: false; + style: "form"; + value: number[]; +} + // @public export interface AddFaceResultOutput { persistedFaceId: string; @@ -211,10 +225,17 @@ export interface AddLargeFaceListFaceFromUrlQueryParam { // @public (undocumented) export interface AddLargeFaceListFaceFromUrlQueryParamProperties { detectionModel?: DetectionModel; - targetFace?: number[]; + targetFace?: number[] | AddLargeFaceListFaceFromUrlTargetFaceQueryParam; userData?: string; } +// @public +export interface AddLargeFaceListFaceFromUrlTargetFaceQueryParam { + explode: false; + style: "form"; + value: number[]; +} + // @public (undocumented) export interface AddLargeFaceListFaceMediaTypesParam { contentType: "application/octet-stream"; @@ -232,10 +253,17 @@ export interface AddLargeFaceListFaceQueryParam { // @public (undocumented) export interface AddLargeFaceListFaceQueryParamProperties { detectionModel?: DetectionModel; - targetFace?: number[]; + targetFace?: number[] | AddLargeFaceListFaceTargetFaceQueryParam; userData?: string; } +// @public +export interface AddLargeFaceListFaceTargetFaceQueryParam { + explode: false; + style: "form"; + value: number[]; +} + // @public export interface AddLargePersonGroupPersonFace200Response extends HttpResponse { // (undocumented) @@ -313,10 +341,17 @@ export interface AddLargePersonGroupPersonFaceFromUrlQueryParam { // @public (undocumented) export interface AddLargePersonGroupPersonFaceFromUrlQueryParamProperties { detectionModel?: DetectionModel; - targetFace?: number[]; + targetFace?: number[] | AddLargePersonGroupPersonFaceFromUrlTargetFaceQueryParam; userData?: string; } +// @public +export interface AddLargePersonGroupPersonFaceFromUrlTargetFaceQueryParam { + explode: false; + style: "form"; + value: number[]; +} + // @public (undocumented) export interface AddLargePersonGroupPersonFaceMediaTypesParam { contentType: "application/octet-stream"; @@ -334,10 +369,17 @@ export interface AddLargePersonGroupPersonFaceQueryParam { // @public (undocumented) export interface AddLargePersonGroupPersonFaceQueryParamProperties { detectionModel?: DetectionModel; - targetFace?: number[]; + targetFace?: number[] | AddLargePersonGroupPersonFaceTargetFaceQueryParam; userData?: string; } +// @public +export interface AddLargePersonGroupPersonFaceTargetFaceQueryParam { + explode: false; + style: "form"; + value: number[]; +} + // @public (undocumented) export interface AddPersonFace { get(options?: GetPersonFacesParameters): StreamableMethod; @@ -444,10 +486,17 @@ export interface AddPersonFaceFromUrlQueryParam { // @public (undocumented) export interface AddPersonFaceFromUrlQueryParamProperties { detectionModel?: DetectionModel; - targetFace?: number[]; + targetFace?: number[] | AddPersonFaceFromUrlTargetFaceQueryParam; userData?: string; } +// @public +export interface AddPersonFaceFromUrlTargetFaceQueryParam { + explode: false; + style: "form"; + value: number[]; +} + // @public export interface AddPersonFaceLogicalResponse extends HttpResponse { // (undocumented) @@ -473,10 +522,17 @@ export interface AddPersonFaceQueryParam { // @public (undocumented) export interface AddPersonFaceQueryParamProperties { detectionModel?: DetectionModel; - targetFace?: number[]; + targetFace?: number[] | AddPersonFaceTargetFaceQueryParam; userData?: string; } +// @public +export interface AddPersonFaceTargetFaceQueryParam { + explode: false; + style: "form"; + value: number[]; +} + // @public export interface AddPersonGroupPersonFace200Response extends HttpResponse { // (undocumented) @@ -554,10 +610,17 @@ export interface AddPersonGroupPersonFaceFromUrlQueryParam { // @public (undocumented) export interface AddPersonGroupPersonFaceFromUrlQueryParamProperties { detectionModel?: DetectionModel; - targetFace?: number[]; + targetFace?: number[] | AddPersonGroupPersonFaceFromUrlTargetFaceQueryParam; userData?: string; } +// @public +export interface AddPersonGroupPersonFaceFromUrlTargetFaceQueryParam { + explode: false; + style: "form"; + value: number[]; +} + // @public (undocumented) export interface AddPersonGroupPersonFaceMediaTypesParam { contentType: "application/octet-stream"; @@ -575,10 +638,17 @@ export interface AddPersonGroupPersonFaceQueryParam { // @public (undocumented) export interface AddPersonGroupPersonFaceQueryParamProperties { detectionModel?: DetectionModel; - targetFace?: number[]; + targetFace?: number[] | AddPersonGroupPersonFaceTargetFaceQueryParam; userData?: string; } +// @public +export interface AddPersonGroupPersonFaceTargetFaceQueryParam { + explode: false; + style: "form"; + value: number[]; +} + // @public export type BlurLevelOutput = string; @@ -1663,12 +1733,19 @@ export interface DetectFromUrlQueryParamProperties { detectionModel?: DetectionModel; faceIdTimeToLive?: number; recognitionModel?: RecognitionModel; - returnFaceAttributes?: FaceAttributeType[]; + returnFaceAttributes?: FaceAttributeType[] | DetectFromUrlReturnFaceAttributesQueryParam; returnFaceId?: boolean; returnFaceLandmarks?: boolean; returnRecognitionModel?: boolean; } +// @public +export interface DetectFromUrlReturnFaceAttributesQueryParam { + explode: false; + style: "form"; + value: FaceAttributeType[]; +} + // @public export type DetectionModel = string; @@ -1691,12 +1768,19 @@ export interface DetectQueryParamProperties { detectionModel?: DetectionModel; faceIdTimeToLive?: number; recognitionModel?: RecognitionModel; - returnFaceAttributes?: FaceAttributeType[]; + returnFaceAttributes?: FaceAttributeType[] | DetectReturnFaceAttributesQueryParam; returnFaceId?: boolean; returnFaceLandmarks?: boolean; returnRecognitionModel?: boolean; } +// @public +export interface DetectReturnFaceAttributesQueryParam { + explode: false; + style: "form"; + value: FaceAttributeType[]; +} + // @public export interface DynamicPersonGroupOutput { readonly dynamicPersonGroupId: string; diff --git a/packages/typespec-test/test/faceai/generated/typespec-ts/src/parameters.ts b/packages/typespec-test/test/faceai/generated/typespec-ts/src/parameters.ts index 00d57ae89a..b2ef5bdb1c 100644 --- a/packages/typespec-test/test/faceai/generated/typespec-ts/src/parameters.ts +++ b/packages/typespec-test/test/faceai/generated/typespec-ts/src/parameters.ts @@ -17,13 +17,25 @@ export interface DetectFromUrlBodyParam { body: { url: string }; } +/** This is the wrapper object for the parameter `returnFaceAttributes` with explode set to false and style set to form. */ +export interface DetectFromUrlReturnFaceAttributesQueryParam { + /** Value of the parameter */ + value: FaceAttributeType[]; + /** Should we explode the value? */ + explode: false; + /** Style of the value */ + style: "form"; +} + export interface DetectFromUrlQueryParamProperties { /** Return faceIds of the detected faces or not. The default value is true. */ returnFaceId?: boolean; /** Return face landmarks of the detected faces or not. The default value is false. */ returnFaceLandmarks?: boolean; /** Analyze and return the one or more specified face attributes in the comma-separated string like 'returnFaceAttributes=headPose,glasses'. Face attribute analysis has additional computational and time cost. */ - returnFaceAttributes?: FaceAttributeType[]; + returnFaceAttributes?: + | FaceAttributeType[] + | DetectFromUrlReturnFaceAttributesQueryParam; /** * The 'recognitionModel' associated with the detected faceIds. Supported 'recognitionModel' values include 'recognition_01', 'recognition_02', 'recognition_03' or 'recognition_04'. The default value is 'recognition_01'. 'recognition_04' is recommended since its accuracy is improved on faces wearing masks compared with 'recognition_03', and its overall accuracy is improved compared with 'recognition_01' and 'recognition_02'. * @@ -69,13 +81,25 @@ export interface DetectBodyParam { | NodeJS.ReadableStream; } +/** This is the wrapper object for the parameter `returnFaceAttributes` with explode set to false and style set to form. */ +export interface DetectReturnFaceAttributesQueryParam { + /** Value of the parameter */ + value: FaceAttributeType[]; + /** Should we explode the value? */ + explode: false; + /** Style of the value */ + style: "form"; +} + export interface DetectQueryParamProperties { /** Return faceIds of the detected faces or not. The default value is true. */ returnFaceId?: boolean; /** Return face landmarks of the detected faces or not. The default value is false. */ returnFaceLandmarks?: boolean; /** Analyze and return the one or more specified face attributes in the comma-separated string like 'returnFaceAttributes=headPose,glasses'. Face attribute analysis has additional computational and time cost. */ - returnFaceAttributes?: FaceAttributeType[]; + returnFaceAttributes?: + | FaceAttributeType[] + | DetectReturnFaceAttributesQueryParam; /** * The 'recognitionModel' associated with the detected faceIds. Supported 'recognitionModel' values include 'recognition_01', 'recognition_02', 'recognition_03' or 'recognition_04'. The default value is 'recognition_01'. 'recognition_04' is recommended since its accuracy is improved on faces wearing masks compared with 'recognition_03', and its overall accuracy is improved compared with 'recognition_01' and 'recognition_02'. * @@ -270,9 +294,19 @@ export interface AddFaceListFaceFromUrlBodyParam { body: { url: string }; } +/** This is the wrapper object for the parameter `targetFace` with explode set to false and style set to form. */ +export interface AddFaceListFaceFromUrlTargetFaceQueryParam { + /** Value of the parameter */ + value: number[]; + /** Should we explode the value? */ + explode: false; + /** Style of the value */ + style: "form"; +} + export interface AddFaceListFaceFromUrlQueryParamProperties { /** A face rectangle to specify the target face to be added to a person, in the format of 'targetFace=left,top,width,height'. */ - targetFace?: number[]; + targetFace?: number[] | AddFaceListFaceFromUrlTargetFaceQueryParam; /** * The 'detectionModel' associated with the detected faceIds. Supported 'detectionModel' values include 'detection_01', 'detection_02' and 'detection_03'. The default value is 'detection_01'. * @@ -305,9 +339,19 @@ export interface AddFaceListFaceBodyParam { | NodeJS.ReadableStream; } +/** This is the wrapper object for the parameter `targetFace` with explode set to false and style set to form. */ +export interface AddFaceListFaceTargetFaceQueryParam { + /** Value of the parameter */ + value: number[]; + /** Should we explode the value? */ + explode: false; + /** Style of the value */ + style: "form"; +} + export interface AddFaceListFaceQueryParamProperties { /** A face rectangle to specify the target face to be added to a person, in the format of 'targetFace=left,top,width,height'. */ - targetFace?: number[]; + targetFace?: number[] | AddFaceListFaceTargetFaceQueryParam; /** * The 'detectionModel' associated with the detected faceIds. Supported 'detectionModel' values include 'detection_01', 'detection_02' and 'detection_03'. The default value is 'detection_01'. * @@ -386,9 +430,19 @@ export interface AddLargeFaceListFaceFromUrlBodyParam { body: { url: string }; } +/** This is the wrapper object for the parameter `targetFace` with explode set to false and style set to form. */ +export interface AddLargeFaceListFaceFromUrlTargetFaceQueryParam { + /** Value of the parameter */ + value: number[]; + /** Should we explode the value? */ + explode: false; + /** Style of the value */ + style: "form"; +} + export interface AddLargeFaceListFaceFromUrlQueryParamProperties { /** A face rectangle to specify the target face to be added to a person, in the format of 'targetFace=left,top,width,height'. */ - targetFace?: number[]; + targetFace?: number[] | AddLargeFaceListFaceFromUrlTargetFaceQueryParam; /** * The 'detectionModel' associated with the detected faceIds. Supported 'detectionModel' values include 'detection_01', 'detection_02' and 'detection_03'. The default value is 'detection_01'. * @@ -421,9 +475,19 @@ export interface AddLargeFaceListFaceBodyParam { | NodeJS.ReadableStream; } +/** This is the wrapper object for the parameter `targetFace` with explode set to false and style set to form. */ +export interface AddLargeFaceListFaceTargetFaceQueryParam { + /** Value of the parameter */ + value: number[]; + /** Should we explode the value? */ + explode: false; + /** Style of the value */ + style: "form"; +} + export interface AddLargeFaceListFaceQueryParamProperties { /** A face rectangle to specify the target face to be added to a person, in the format of 'targetFace=left,top,width,height'. */ - targetFace?: number[]; + targetFace?: number[] | AddLargeFaceListFaceTargetFaceQueryParam; /** * The 'detectionModel' associated with the detected faceIds. Supported 'detectionModel' values include 'detection_01', 'detection_02' and 'detection_03'. The default value is 'detection_01'. * @@ -554,9 +618,19 @@ export interface AddPersonGroupPersonFaceFromUrlBodyParam { body: { url: string }; } +/** This is the wrapper object for the parameter `targetFace` with explode set to false and style set to form. */ +export interface AddPersonGroupPersonFaceFromUrlTargetFaceQueryParam { + /** Value of the parameter */ + value: number[]; + /** Should we explode the value? */ + explode: false; + /** Style of the value */ + style: "form"; +} + export interface AddPersonGroupPersonFaceFromUrlQueryParamProperties { /** A face rectangle to specify the target face to be added to a person, in the format of 'targetFace=left,top,width,height'. */ - targetFace?: number[]; + targetFace?: number[] | AddPersonGroupPersonFaceFromUrlTargetFaceQueryParam; /** * The 'detectionModel' associated with the detected faceIds. Supported 'detectionModel' values include 'detection_01', 'detection_02' and 'detection_03'. The default value is 'detection_01'. * @@ -589,9 +663,19 @@ export interface AddPersonGroupPersonFaceBodyParam { | NodeJS.ReadableStream; } +/** This is the wrapper object for the parameter `targetFace` with explode set to false and style set to form. */ +export interface AddPersonGroupPersonFaceTargetFaceQueryParam { + /** Value of the parameter */ + value: number[]; + /** Should we explode the value? */ + explode: false; + /** Style of the value */ + style: "form"; +} + export interface AddPersonGroupPersonFaceQueryParamProperties { /** A face rectangle to specify the target face to be added to a person, in the format of 'targetFace=left,top,width,height'. */ - targetFace?: number[]; + targetFace?: number[] | AddPersonGroupPersonFaceTargetFaceQueryParam; /** * The 'detectionModel' associated with the detected faceIds. Supported 'detectionModel' values include 'detection_01', 'detection_02' and 'detection_03'. The default value is 'detection_01'. * @@ -709,9 +793,21 @@ export interface AddLargePersonGroupPersonFaceFromUrlBodyParam { body: { url: string }; } +/** This is the wrapper object for the parameter `targetFace` with explode set to false and style set to form. */ +export interface AddLargePersonGroupPersonFaceFromUrlTargetFaceQueryParam { + /** Value of the parameter */ + value: number[]; + /** Should we explode the value? */ + explode: false; + /** Style of the value */ + style: "form"; +} + export interface AddLargePersonGroupPersonFaceFromUrlQueryParamProperties { /** A face rectangle to specify the target face to be added to a person, in the format of 'targetFace=left,top,width,height'. */ - targetFace?: number[]; + targetFace?: + | number[] + | AddLargePersonGroupPersonFaceFromUrlTargetFaceQueryParam; /** * The 'detectionModel' associated with the detected faceIds. Supported 'detectionModel' values include 'detection_01', 'detection_02' and 'detection_03'. The default value is 'detection_01'. * @@ -744,9 +840,19 @@ export interface AddLargePersonGroupPersonFaceBodyParam { | NodeJS.ReadableStream; } +/** This is the wrapper object for the parameter `targetFace` with explode set to false and style set to form. */ +export interface AddLargePersonGroupPersonFaceTargetFaceQueryParam { + /** Value of the parameter */ + value: number[]; + /** Should we explode the value? */ + explode: false; + /** Style of the value */ + style: "form"; +} + export interface AddLargePersonGroupPersonFaceQueryParamProperties { /** A face rectangle to specify the target face to be added to a person, in the format of 'targetFace=left,top,width,height'. */ - targetFace?: number[]; + targetFace?: number[] | AddLargePersonGroupPersonFaceTargetFaceQueryParam; /** * The 'detectionModel' associated with the detected faceIds. Supported 'detectionModel' values include 'detection_01', 'detection_02' and 'detection_03'. The default value is 'detection_01'. * @@ -835,9 +941,19 @@ export interface AddPersonFaceBodyParam { | NodeJS.ReadableStream; } +/** This is the wrapper object for the parameter `targetFace` with explode set to false and style set to form. */ +export interface AddPersonFaceTargetFaceQueryParam { + /** Value of the parameter */ + value: number[]; + /** Should we explode the value? */ + explode: false; + /** Style of the value */ + style: "form"; +} + export interface AddPersonFaceQueryParamProperties { /** A face rectangle to specify the target face to be added to a person, in the format of 'targetFace=left,top,width,height'. */ - targetFace?: number[]; + targetFace?: number[] | AddPersonFaceTargetFaceQueryParam; /** * The 'detectionModel' associated with the detected faceIds. Supported 'detectionModel' values include 'detection_01', 'detection_02' and 'detection_03'. The default value is 'detection_01'. * @@ -866,9 +982,19 @@ export interface AddPersonFaceFromUrlBodyParam { body: { url: string }; } +/** This is the wrapper object for the parameter `targetFace` with explode set to false and style set to form. */ +export interface AddPersonFaceFromUrlTargetFaceQueryParam { + /** Value of the parameter */ + value: number[]; + /** Should we explode the value? */ + explode: false; + /** Style of the value */ + style: "form"; +} + export interface AddPersonFaceFromUrlQueryParamProperties { /** A face rectangle to specify the target face to be added to a person, in the format of 'targetFace=left,top,width,height'. */ - targetFace?: number[]; + targetFace?: number[] | AddPersonFaceFromUrlTargetFaceQueryParam; /** * The 'detectionModel' associated with the detected faceIds. Supported 'detectionModel' values include 'detection_01', 'detection_02' and 'detection_03'. The default value is 'detection_01'. * diff --git a/packages/typespec-test/test/translator/generated/typespec-ts/review/cognitiveservices-translator.api.md b/packages/typespec-test/test/translator/generated/typespec-ts/review/cognitiveservices-translator.api.md index 8fbcf87aab..d8cfc7be25 100644 --- a/packages/typespec-test/test/translator/generated/typespec-ts/review/cognitiveservices-translator.api.md +++ b/packages/typespec-test/test/translator/generated/typespec-ts/review/cognitiveservices-translator.api.md @@ -591,10 +591,17 @@ export interface TranslateQueryParamProperties { profanityMarker?: ProfanityMarkers; suggestedFrom?: string; textType?: TextTypes; - to: string; + to: TranslateToQueryParam | string; toScript?: string; } +// @public +export interface TranslateToQueryParam { + explode: true; + style: "form"; + value: string[]; +} + // @public export interface TranslationLanguageOutput { dir: string; diff --git a/packages/typespec-test/test/translator/generated/typespec-ts/src/parameters.ts b/packages/typespec-test/test/translator/generated/typespec-ts/src/parameters.ts index d3e9811615..97372fdbd1 100644 --- a/packages/typespec-test/test/translator/generated/typespec-ts/src/parameters.ts +++ b/packages/typespec-test/test/translator/generated/typespec-ts/src/parameters.ts @@ -63,14 +63,25 @@ export interface TranslateBodyParam { body: Array; } +/** This is the wrapper object for the parameter `to` with explode set to true and style set to form. */ +export interface TranslateToQueryParam { + /** Value of the parameter */ + value: string[]; + /** Should we explode the value? */ + explode: true; + /** Style of the value */ + style: "form"; +} + export interface TranslateQueryParamProperties { /** * Specifies the language of the output text. The target language must be one of the supported languages included * in the translation scope. For example, use to=de to translate to German. * It's possible to translate to multiple languages simultaneously by repeating the parameter in the query string. - * For example, use to=de&to=it to translate to German and Italian. This parameter needs to be formatted as multi collection, we provide buildMultiCollection from serializeHelper.ts to help, you will probably need to set skipUrlEncoding as true when sending the request + * For example, use to=de&to=it to translate to German and Italian. + * This parameter could be formatted as multi collection string, we provide buildMultiCollection from serializeHelper.ts to help, you will probably need to set skipUrlEncoding as true when sending the request. */ - to: string; + to: TranslateToQueryParam | string; /** * Specifies the language of the input text. Find which languages are available to translate from by * looking up supported languages using the translation scope. If the from parameter isn't specified, diff --git a/packages/typespec-test/test/translator/tspconfig.yaml b/packages/typespec-test/test/translator/tspconfig.yaml index 5ef8fd2d4d..4b682a37d0 100644 --- a/packages/typespec-test/test/translator/tspconfig.yaml +++ b/packages/typespec-test/test/translator/tspconfig.yaml @@ -6,6 +6,7 @@ options: generateMetadata: true generateTest: true azureSdkForJs: false + compatibilityQueryMultiFormat: true packageDetails: name: "@azure-rest/cognitiveservices-translator" description: "Microsoft Translator Service" diff --git a/packages/typespec-ts/README.md b/packages/typespec-ts/README.md index abe45851dc..ce04624ce9 100644 --- a/packages/typespec-ts/README.md +++ b/packages/typespec-ts/README.md @@ -153,6 +153,22 @@ If we enable this option `clearOutputFolder` we would empty the whole output fol clearOutputFolder: true ``` +### compatibilityMode + +By default, this option will be disabled. If this option is enabled, it will affect the generation of the additional property feature for the Modular client. + +```yaml +compatibilityMode: true +``` + +### compatibilityQueryMultiFormat + +By default, this option will be disabled. If this option is enabled, we should generate the backward-compatible code for query parameter serialization for array types in RLC. + +```yaml +compatibilityQueryMultiFormat: true +``` + # Contributing If you want to contribute on this project read the [contrubuting document](./CONTRIBUTING.md) for more details. diff --git a/packages/typespec-ts/src/lib.ts b/packages/typespec-ts/src/lib.ts index 8a6b1285ce..e285edbe50 100644 --- a/packages/typespec-ts/src/lib.ts +++ b/packages/typespec-ts/src/lib.ts @@ -92,7 +92,8 @@ export const RLCOptionsSchema: JSONSchemaType = { compatibilityMode: { type: "boolean", nullable: true }, experimentalExtensibleEnums: { type: "boolean", nullable: true }, clearOutputFolder: { type: "boolean", nullable: true }, - ignorePropertyNameNormalize: { type: "boolean", nullable: true } + ignorePropertyNameNormalize: { type: "boolean", nullable: true }, + compatibilityQueryMultiFormat: { type: "boolean", nullable: true } }, required: [] }; @@ -268,6 +269,12 @@ const libDef = { default: paramMessage`Path parameter '${"paramName"}' cannot be optional.` } }, + "un-supported-format-cases": { + severity: "warning", + messages: { + default: paramMessage`The parameter ${"paramName"} with explode: ${"explode"} and format: ${"format"} is not supported.` + } + }, "parameter-type-not-supported": { severity: "warning", messages: { diff --git a/packages/typespec-ts/src/transform/transformHelperFunctionDetails.ts b/packages/typespec-ts/src/transform/transformHelperFunctionDetails.ts index e7c1cee968..413f4a7984 100644 --- a/packages/typespec-ts/src/transform/transformHelperFunctionDetails.ts +++ b/packages/typespec-ts/src/transform/transformHelperFunctionDetails.ts @@ -177,33 +177,19 @@ function extractSpecialSerializeInfo( dpgContext: SdkContext ) { let hasMultiCollection = false; - let hasPipeCollection = false; - let hasTsvCollection = false; - let hasSsvCollection = false; let hasCsvCollection = false; const clientOperations = listOperationsInOperationGroup(dpgContext, client); for (const clientOp of clientOperations) { const route = getHttpOperationWithCache(dpgContext, clientOp); route.parameters.parameters.forEach((parameter) => { const serializeInfo = getSpecialSerializeInfo( + dpgContext, parameter.type, (parameter as any).format ); hasMultiCollection = hasMultiCollection ? hasMultiCollection : serializeInfo.hasMultiCollection; - hasPipeCollection = hasPipeCollection - ? hasPipeCollection - : serializeInfo.hasPipeCollection; - hasTsvCollection = hasTsvCollection - ? hasTsvCollection - : serializeInfo.hasTsvCollection; - hasSsvCollection = hasSsvCollection - ? hasSsvCollection - : serializeInfo.hasSsvCollection; - hasCsvCollection = hasCsvCollection - ? hasCsvCollection - : serializeInfo.hasCsvCollection; }); } const operationGroups = listOperationGroups(dpgContext, client, true); @@ -216,21 +202,13 @@ function extractSpecialSerializeInfo( const route = getHttpOperationWithCache(dpgContext, op); route.parameters.parameters.forEach((parameter) => { const serializeInfo = getSpecialSerializeInfo( + dpgContext, parameter.type, (parameter as any).format ); hasMultiCollection = hasMultiCollection ? hasMultiCollection : serializeInfo.hasMultiCollection; - hasPipeCollection = hasPipeCollection - ? hasPipeCollection - : serializeInfo.hasPipeCollection; - hasTsvCollection = hasTsvCollection - ? hasTsvCollection - : serializeInfo.hasTsvCollection; - hasSsvCollection = hasSsvCollection - ? hasSsvCollection - : serializeInfo.hasSsvCollection; hasCsvCollection = hasCsvCollection ? hasCsvCollection : serializeInfo.hasCsvCollection; @@ -239,9 +217,6 @@ function extractSpecialSerializeInfo( } return { hasMultiCollection, - hasPipeCollection, - hasTsvCollection, - hasSsvCollection, hasCsvCollection }; } diff --git a/packages/typespec-ts/src/transform/transformParameters.ts b/packages/typespec-ts/src/transform/transformParameters.ts index 1f1064d91d..0dbdb5177d 100644 --- a/packages/typespec-ts/src/transform/transformParameters.ts +++ b/packages/typespec-ts/src/transform/transformParameters.ts @@ -35,8 +35,8 @@ import { getFormattedPropertyDoc, getImportedModelName, getSchemaForType, - getSerializeTypeName, getTypeName, + isArrayType, isBodyRequired } from "../utils/modelUtils.js"; import { @@ -44,10 +44,17 @@ import { getOperationName, getSpecialSerializeInfo } from "../utils/operationUtil.js"; - import { SdkContext } from "../utils/interfaces.js"; +import { getParameterSerializationInfo } from "../utils/parameterUtils.js"; import { reportDiagnostic } from "../lib.js"; +interface ParameterTransformationOptions { + apiVersionInfo?: ApiVersionInfo; + operationGroupName?: string; + operationName?: string; + importModels?: Set; +} + export function transformToParameterTypes( client: SdkClient, dpgContext: SdkContext, @@ -91,22 +98,27 @@ export function transformToParameterTypes( operationName: getOperationName(dpgContext, route.operation), parameters: [] }; + const options = { + apiVersionInfo, + operationGroupName: rlcParameter.operationGroup, + operationName: rlcParameter.operationName, + importModels: outputImportedSet + }; // transform query param const queryParams = transformQueryParameters( dpgContext, parameters, - { apiVersionInfo }, - outputImportedSet + options ); // transform path param - const pathParams = transformPathParameters(); + const pathParams = transformPathParameters(dpgContext, parameters, options); // TODO: support cookie parameters, https://github.com/Azure/autorest.typescript/issues/2898 transformCookieParameters(dpgContext, parameters); // transform header param including content-type const headerParams = transformHeaderParameters( dpgContext, parameters, - outputImportedSet + options ); // transform body const bodyType = getBodyType(route); @@ -133,65 +145,62 @@ function getParameterMetadata( dpgContext: SdkContext, paramType: "query" | "path" | "header", parameter: HttpOperationParameter, - importedModels: Set + options: ParameterTransformationOptions ): ParameterMetadata { const program = dpgContext.program; + const importedModels = options.importModels ?? new Set(); const schemaContext = [SchemaContext.Exception, SchemaContext.Input]; const schema = getSchemaForType(dpgContext, parameter.param.type, { usage: schemaContext, needRef: false, relevantProperty: parameter.param }) as Schema; - let type = getTypeName(schema, schemaContext); const name = getParameterName(parameter.name); let description = getFormattedPropertyDoc(program, parameter.param, schema) ?? ""; - if ( - type === "string[]" || - type === "Array" || - type === "number[]" || - type === "Array" - ) { + if (isArrayType(schema)) { const serializeInfo = getSpecialSerializeInfo( + dpgContext, parameter.type, (parameter as any).format ); - if ( - serializeInfo.hasMultiCollection || - serializeInfo.hasPipeCollection || - serializeInfo.hasSsvCollection || - serializeInfo.hasTsvCollection || - serializeInfo.hasCsvCollection - ) { - type = "string"; - description += ` This parameter needs to be formatted as ${serializeInfo.collectionInfo.join( + if (serializeInfo.hasMultiCollection || serializeInfo.hasCsvCollection) { + description += `${description ? "\n" : ""}This parameter could be formatted as ${serializeInfo.collectionInfo.join( ", " - )} collection, we provide ${serializeInfo.descriptions.join( + )} collection string, we provide ${serializeInfo.descriptions.join( ", " )} from serializeHelper.ts to help${ serializeInfo.hasMultiCollection ? ", you will probably need to set skipUrlEncoding as true when sending the request" : "" - }`; + }.`; + } + if ((parameter as any).format === "tsv") { + description += `${description ? "\n" : ""}This parameter could be formatted as tsv collection string.`; } } - type = - paramType !== "query" && type !== "string" - ? getSerializeTypeName(dpgContext.program, schema, schemaContext) - : type; + getImportedModelName(schema, schemaContext)?.forEach( importedModels.add, importedModels ); + const serializationType = getParameterSerializationInfo( + dpgContext, + parameter, + schema, + options.operationGroupName, + options.operationName + ); return { type: paramType, name, param: { name, - type, - typeName: type, + type: serializationType.typeName, + typeName: serializationType.typeName, required: !parameter.param.optional, - description + description, + wrapperType: serializationType.wrapperType } }; } @@ -225,8 +234,7 @@ function transformCookieParameters( function transformQueryParameters( dpgContext: SdkContext, parameters: HttpOperationParameters, - options: { apiVersionInfo: ApiVersionInfo | undefined }, - importModels: Set = new Set() + options: ParameterTransformationOptions ): ParameterMetadata[] { const queryParameters = parameters.parameters.filter( (p) => @@ -240,7 +248,7 @@ function transformQueryParameters( return []; } return queryParameters.map((qp) => - getParameterMetadata(dpgContext, "query", qp, importModels) + getParameterMetadata(dpgContext, "query", qp, options) ); } @@ -248,16 +256,27 @@ function transformQueryParameters( * Only support to take the global path parameter as path parameter * @returns */ -function transformPathParameters() { - // TODO - // issue tracked https://github.com/Azure/autorest.typescript/issues/1521 - return []; +function transformPathParameters( + dpgContext: SdkContext, + parameters: HttpOperationParameters, + options: ParameterTransformationOptions +) { + // build wrapper path parameters + const pathParameters = parameters.parameters.filter((p) => p.type === "path"); + if (!pathParameters.length) { + return []; + } + // only need to build path parameters for wrapper type + const params = pathParameters + .map((qp) => getParameterMetadata(dpgContext, "path", qp, options)) + .filter((p) => p.param.wrapperType); + return params; } export function transformHeaderParameters( dpgContext: SdkContext, parameters: HttpOperationParameters, - importedModels: Set + options: ParameterTransformationOptions ): ParameterMetadata[] { const headerParameters = parameters.parameters.filter( (p) => p.type === "header" @@ -266,7 +285,7 @@ export function transformHeaderParameters( return []; } return headerParameters.map((qp) => - getParameterMetadata(dpgContext, "header", qp, importedModels) + getParameterMetadata(dpgContext, "header", qp, options) ); } diff --git a/packages/typespec-ts/src/transform/transformPaths.ts b/packages/typespec-ts/src/transform/transformPaths.ts index d6b97eddb8..9fb441c2c2 100644 --- a/packages/typespec-ts/src/transform/transformPaths.ts +++ b/packages/typespec-ts/src/transform/transformPaths.ts @@ -31,12 +31,12 @@ import { import { getImportedModelName, getSchemaForType, - getTypeName, isBodyRequired } from "../utils/modelUtils.js"; import { SdkContext } from "../utils/interfaces.js"; import { getDoc } from "@typespec/compiler"; +import { getParameterSerializationInfo } from "../utils/parameterUtils.js"; export function transformPaths( client: SdkClient, @@ -133,15 +133,33 @@ function transformOperation( .filter((p) => p.type === "path") .map((p) => { const schemaUsage = [SchemaContext.Input, SchemaContext.Exception]; + const options = { + usage: schemaUsage, + needRef: false, + relevantProperty: p.param + }; const schema = p.param.sourceProperty - ? getSchemaForType(dpgContext, p.param.sourceProperty?.type) - : getSchemaForType(dpgContext, p.param.type); + ? getSchemaForType( + dpgContext, + p.param.sourceProperty?.type, + + options + ) + : getSchemaForType(dpgContext, p.param.type, options); const importedNames = getImportedModelName(schema, schemaUsage) ?? []; importedNames.forEach(importSet.add, importSet); + const serializationType = getParameterSerializationInfo( + dpgContext, + p, + schema, + operationGroupName, + method.operationName + ); return { name: p.name, - type: getTypeName(schema, schemaUsage), - description: getDoc(program, p.param) + type: serializationType.typeName, + description: getDoc(program, p.param) ?? "", + wrapperType: serializationType.wrapperType }; }), operationGroupName: getOperationGroupName(dpgContext, route), diff --git a/packages/typespec-ts/src/utils/modelUtils.ts b/packages/typespec-ts/src/utils/modelUtils.ts index 6befef524d..c902935f96 100644 --- a/packages/typespec-ts/src/utils/modelUtils.ts +++ b/packages/typespec-ts/src/utils/modelUtils.ts @@ -1132,6 +1132,18 @@ function isUnionType(type: Type) { return type.kind === "Union"; } +export function isObjectOrDictType(schema: Schema) { + return ( + (schema.type === "object" && + (schema as ObjectSchema).properties !== undefined) || + schema.type === "dictionary" + ); +} + +export function isArrayType(schema: Schema) { + return schema.type === "array"; +} + function getSchemaForStdScalar( program: Program, type: Scalar, @@ -1331,38 +1343,6 @@ export function getTypeName(schema: Schema, usage?: SchemaContext[]): string { return getPriorityName(schema, usage) ?? schema.type ?? "any"; } -export function getSerializeTypeName( - program: Program, - schema: Schema, - usage?: SchemaContext[] -): string { - const typeName = getTypeName(schema, usage); - const formattedName = (schema.alias ?? typeName).replace( - "Date | string", - "string" - ); - const canSerialize = isSerializable(schema); - if (canSerialize) { - return schema.alias ? typeName : formattedName; - } - reportDiagnostic(program, { - code: "unable-serialized-type", - format: { type: typeName }, - target: NoTarget - }); - return "string"; - function isSerializable(type: any) { - if (type.enum) { - return type.enum.every((i: any) => { - return isSerializable(i) || i.type === "null"; - }); - } - return ( - ["string", "number", "boolean"].includes(type.type) || type.isConstant - ); - } -} - export function getImportedModelName( schema: Schema, usage?: SchemaContext[] @@ -1448,7 +1428,7 @@ function getEnumStringDescription(type: any) { return undefined; } -function getBinaryDescripton(type: any) { +function getBinaryDescription(type: any) { if (type?.typeName?.includes(BINARY_TYPE_UNION)) { return `Value may contain any sequence of octets`; } @@ -1480,7 +1460,7 @@ export function getFormattedPropertyDoc( const enhancedDocFromType = getEnumStringDescription(schemaType) ?? getDecimalDescription(schemaType) ?? - getBinaryDescripton(schemaType); + getBinaryDescription(schemaType); if (propertyDoc && enhancedDocFromType) { return `${propertyDoc}${sperator}${enhancedDocFromType}`; } diff --git a/packages/typespec-ts/src/utils/operationUtil.ts b/packages/typespec-ts/src/utils/operationUtil.ts index fd46ee08ec..3a6217ee34 100644 --- a/packages/typespec-ts/src/utils/operationUtil.ts +++ b/packages/typespec-ts/src/utils/operationUtil.ts @@ -409,13 +409,16 @@ export function extractPagedMetadataNested( } export function getSpecialSerializeInfo( + dpgContext: SdkContext, paramType: string, paramFormat: string ) { - const hasMultiCollection = getHasMultiCollection(paramType, paramFormat); - const hasPipeCollection = getHasPipeCollection(paramType, paramFormat); - const hasSsvCollection = getHasSsvCollection(paramType, paramFormat); - const hasTsvCollection = getHasTsvCollection(paramType, paramFormat); + const hasMultiCollection = getHasMultiCollection( + paramType, + paramFormat, + // Include query multi support in compatibility mode + dpgContext.rlcOptions?.compatibilityQueryMultiFormat ?? false + ); const hasCsvCollection = getHasCsvCollection(paramType, paramFormat); const descriptions = []; const collectionInfo = []; @@ -423,20 +426,6 @@ export function getSpecialSerializeInfo( descriptions.push("buildMultiCollection"); collectionInfo.push("multi"); } - if (hasSsvCollection) { - descriptions.push("buildSsvCollection"); - collectionInfo.push("ssv"); - } - - if (hasTsvCollection) { - descriptions.push("buildTsvCollection"); - collectionInfo.push("tsv"); - } - - if (hasPipeCollection) { - descriptions.push("buildPipeCollection"); - collectionInfo.push("pipe"); - } if (hasCsvCollection) { descriptions.push("buildCsvCollection"); @@ -444,18 +433,20 @@ export function getSpecialSerializeInfo( } return { hasMultiCollection, - hasPipeCollection, - hasSsvCollection, - hasTsvCollection, hasCsvCollection, descriptions, collectionInfo }; } -function getHasMultiCollection(paramType: string, paramFormat: string) { +function getHasMultiCollection( + paramType: string, + paramFormat: string, + includeQuery = true +) { return ( - (paramType === "query" || paramType === "header") && paramFormat === "multi" + ((includeQuery && paramType === "query") || paramType === "header") && + paramFormat === "multi" ); } function getHasSsvCollection(paramType: string, paramFormat: string) { diff --git a/packages/typespec-ts/src/utils/parameterUtils.ts b/packages/typespec-ts/src/utils/parameterUtils.ts new file mode 100644 index 0000000000..15d78f37e1 --- /dev/null +++ b/packages/typespec-ts/src/utils/parameterUtils.ts @@ -0,0 +1,234 @@ +import { + NameType, + normalizeName, + Schema, + SchemaContext +} from "@azure-tools/rlc-common"; +import { HttpOperationParameter } from "@typespec/http"; +import { getTypeName, isArrayType, isObjectOrDictType } from "./modelUtils.js"; +import { SdkContext } from "./interfaces.js"; +import { reportDiagnostic } from "../lib.js"; +import { NoTarget, Program } from "@typespec/compiler"; + +export interface ParameterSerializationInfo { + typeName: string; + wrapperType?: Schema; +} + +export function getParameterSerializationInfo( + dpgContext: SdkContext, + parameter: HttpOperationParameter, + valueSchema: Schema, + operationGroup: string = "", + operationName: string = "" +): ParameterSerializationInfo { + const schemaContext = [SchemaContext.Input]; + const retVal: ParameterSerializationInfo = buildSerializationInfo( + getTypeName(valueSchema, schemaContext) + ); + const prefix = `${operationGroup}_${operationName}_${parameter.name}`; + switch (parameter.type) { + case "path": { + if (parameter.allowReserved === true) { + const wrapperType = buildAllowReserved( + normalizeName(`${prefix}_PathParam`, NameType.Interface), + valueSchema, + parameter.name + ); + return buildSerializationInfo(wrapperType); + } + return retVal; + } + case "query": { + if (!isArrayType(valueSchema) && !isObjectOrDictType(valueSchema)) { + // if the value is a primitive type or other types, we will not generate a wrapper type + return retVal; + } + const name = normalizeName(`${prefix}_QueryParam`, NameType.Interface); + if (parameter.explode === true) { + if (parameter.format !== undefined && parameter.format !== "multi") { + reportDiagnostic(dpgContext.program, { + code: "un-supported-format-cases", + format: { + paramName: parameter.name, + explode: String(parameter.explode), + format: parameter.format + }, + target: NoTarget + }); + } + let wrapperType: Schema = buildExplodeAndStyle( + name, + true, + "form", + valueSchema, + parameter.name + ); + if (dpgContext.rlcOptions?.compatibilityQueryMultiFormat) { + wrapperType = buildUnionType([ + wrapperType, + { type: "string", name: "string" } + ]); + } + return buildSerializationInfo(wrapperType); + } + + if (parameter.format === undefined || parameter.format === "csv") { + let wrapperType: Schema = buildExplodeAndStyle( + name, + false, + "form", + valueSchema, + parameter.name + ); + if (isArrayType(valueSchema)) { + wrapperType = buildUnionType([valueSchema, wrapperType]); + } + return buildSerializationInfo(wrapperType); + } else if (parameter.format === "ssv" || parameter.format === "pipes") { + const wrapperType = buildExplodeAndStyle( + name, + false, + parameter.format === "ssv" ? "spaceDelimited" : "pipeDelimited", + valueSchema, + parameter.name + ); + return buildSerializationInfo(wrapperType); + } else { + reportDiagnostic(dpgContext.program, { + code: "un-supported-format-cases", + format: { + paramName: parameter.name, + explode: String(parameter.explode), + format: parameter.format + }, + target: NoTarget + }); + } + return buildSerializationInfo("string"); + } + case "header": { + return buildSerializationInfo( + getHeaderSerializeTypeName( + dpgContext.program, + valueSchema, + schemaContext + ) + ); + } + default: + return retVal; + } +} + +function buildSerializationInfo( + typeOrSchema: string | Schema +): ParameterSerializationInfo { + if (typeof typeOrSchema === "string") { + return { + typeName: typeOrSchema + }; + } + return { + typeName: getTypeName(typeOrSchema), + wrapperType: typeOrSchema + }; +} + +function getHeaderSerializeTypeName( + program: Program, + schema: Schema, + usage?: SchemaContext[] +): string { + const typeName = getTypeName(schema, usage); + const formattedName = (schema.alias ?? typeName).replace( + "Date | string", + "string" + ); + const canSerialize = isSerializable(schema); + if (canSerialize) { + return schema.alias ? typeName : formattedName; + } + reportDiagnostic(program, { + code: "unable-serialized-type", + format: { type: typeName }, + target: NoTarget + }); + return "string"; + function isSerializable(type: any) { + if (type.enum) { + return type.enum.every((i: any) => { + return isSerializable(i) || i.type === "null"; + }); + } + return ( + ["string", "number", "boolean"].includes(type.type) || type.isConstant + ); + } +} + +function buildAllowReserved( + typeName: string, + valueSchema: Schema, + parameterName: string +) { + return { + type: "object", + name: typeName, + description: `This is the wrapper object for the parameter \`${parameterName}\` with allowReserved set to true.`, + properties: { + value: { + ...valueSchema, + description: valueSchema.description ?? "Value of the parameter", + required: true + }, + allowReserved: { + type: `true`, + description: "Whether to allow reserved characters", + required: true + } + } + }; +} + +function buildExplodeAndStyle( + typeName: string, + explode: boolean, + style: string, + valueSchema: Schema, + parameterName: string +) { + return { + type: "object", + name: typeName, + description: `This is the wrapper object for the parameter \`${parameterName}\` with explode set to ${explode} and style set to ${style}.`, + properties: { + value: { + ...valueSchema, + description: valueSchema.description ?? "Value of the parameter", + required: true + }, + explode: { + type: `${explode}`, + description: "Should we explode the value?", + required: true + }, + style: { + type: `"${style}"`, + description: "Style of the value", + required: true + } + } + }; +} + +function buildUnionType(values: Schema[]) { + return { + name: "", + enum: values, + type: "union", + typeName: `${values.map((v) => getTypeName(v)).join(" | ")}`, + required: true, + description: undefined + }; +} diff --git a/packages/typespec-ts/test/integration/azurecore.spec.ts b/packages/typespec-ts/test/integration/azurecore.spec.ts index 65a6e0e0e8..f49199f258 100644 --- a/packages/typespec-ts/test/integration/azurecore.spec.ts +++ b/packages/typespec-ts/test/integration/azurecore.spec.ts @@ -1,9 +1,17 @@ import { assert } from "chai"; import AzureCoreClientFactory, { AzureCoreClient, - buildMultiCollection, isUnexpected } from "./generated/azure/core/basic/src/index.js"; + +function buildExplodedFormStyle(values: string[]) { + return { + value: values, + explode: true, + style: "form" + } as const; +} + describe("Azure Core Rest Client", () => { let client: AzureCoreClient; @@ -92,42 +100,41 @@ describe("Azure Core Rest Client", () => { }); it("should list users", async () => { - try { - const result = await client.path("/azure/core/basic/users").get({ - queryParameters: { - top: 5, - skip: 10, - orderby: "id", - filter: "id lt 10", - select: buildMultiCollection(["id", "orders", "etag"], "select"), - expand: "orders" + const result = await client.path("/azure/core/basic/users").get({ + queryParameters: { + top: 5, + skip: 10, + orderby: { + value: ["id"], + explode: true, + style: "form" }, - skipUrlEncoding: true - }); - if (isUnexpected(result)) { - throw Error("Unexpected status code"); + filter: "id lt 10", + select: buildExplodedFormStyle(["id", "orders", "etag"]), + expand: buildExplodedFormStyle(["orders"]) } - const responseBody = { - value: [ - { - id: 1, - name: "Madge", - etag: "11bdc430-65e8-45ad-81d9-8ffa60d55b59", - orders: [{ id: 1, userId: 1, detail: "a recorder" }] - }, - { - id: 2, - name: "John", - etag: "11bdc430-65e8-45ad-81d9-8ffa60d55b5a", - orders: [{ id: 2, userId: 2, detail: "a TV" }] - } - ] - }; - assert.strictEqual(result.status, "200"); - assert.deepEqual(result.body, responseBody); - } catch (err) { - assert.fail(err as string); + }); + if (isUnexpected(result)) { + throw Error("Unexpected status code"); } + const responseBody = { + value: [ + { + id: 1, + name: "Madge", + etag: "11bdc430-65e8-45ad-81d9-8ffa60d55b59", + orders: [{ id: 1, userId: 1, detail: "a recorder" }] + }, + { + id: 2, + name: "John", + etag: "11bdc430-65e8-45ad-81d9-8ffa60d55b5a", + orders: [{ id: 2, userId: 2, detail: "a TV" }] + } + ] + }; + assert.strictEqual(result.status, "200"); + assert.deepEqual(result.body, responseBody); }); it("should export a user", async () => { diff --git a/packages/typespec-ts/test/integration/collectionFormat.spec.ts b/packages/typespec-ts/test/integration/collectionFormat.spec.ts index 40ee4f266e..65ab7d6d9d 100644 --- a/packages/typespec-ts/test/integration/collectionFormat.spec.ts +++ b/packages/typespec-ts/test/integration/collectionFormat.spec.ts @@ -2,10 +2,7 @@ import { assert } from "chai"; import CollectionFormatClientFactory, { buildCsvCollection, buildMultiCollection, - buildPipeCollection, - buildSsvCollection, - buildTsvCollection, - CollectionFormatClient + CollectionFormatClient, } from "./generated/parameters/collection-format/src/index.js"; describe("Collection Format Rest Client", () => { let client: CollectionFormatClient; @@ -20,111 +17,98 @@ describe("Collection Format Rest Client", () => { }); }); + it("[legacy]should serialize multi format query array parameter with buildMultiCollection helper", async () => { + const result = await client + .path("/parameters/collection-format/query/multi") + .get({ + queryParameters: { + colors: buildMultiCollection(colors, "colors") + }, + skipUrlEncoding: true + }); + assert.strictEqual(result.status, "204"); + }); + it("should serialize multi format query array parameter", async () => { - try { - const result = await client - .path("/parameters/collection-format/query/multi") - .get({ - queryParameters: { - colors: buildMultiCollection(colors, "colors") - }, - skipUrlEncoding: true - }); - assert.strictEqual(result.status, "204"); - } catch (err) { - assert.fail(err as string); - } + const result = await client + .path("/parameters/collection-format/query/multi") + .get({ + queryParameters: { + colors: { + value: colors, + explode: true, + style: "form" + } + } + }); + assert.strictEqual(result.status, "204"); }); - it("should serialize csv format query array parameter", async () => { - try { - const result = await client - .path("/parameters/collection-format/query/csv") - .get({ - queryParameters: { - colors: colors - }, - skipUrlEncoding: true - }); - assert.strictEqual(result.status, "204"); - } catch (err) { - assert.fail(err as string); - } + it("should serialize csv format query array parameter with array type", async () => { + const result = await client + .path("/parameters/collection-format/query/csv") + .get({ + queryParameters: { + colors + } + }); + assert.strictEqual(result.status, "204"); }); - it("should serialize ssv format query array parameter", async () => { - try { - const result = await client - .path("/parameters/collection-format/query/ssv") - .get({ - queryParameters: { - colors: buildSsvCollection(colors) + it("should serialize csv format query array parameter with wrapper types", async () => { + const result = await client + .path("/parameters/collection-format/query/csv") + .get({ + queryParameters: { + colors: { + value: colors, + explode: false, + style: "form" } - }); - assert.strictEqual(result.status, "204"); - } catch (err) { - assert.fail(err as string); - } + } + }); + assert.strictEqual(result.status, "204"); }); - it("should serialize tsv format query array parameter", async () => { - try { - const result = await client - .path("/parameters/collection-format/query/tsv") - .get({ - queryParameters: { - colors: buildTsvCollection(colors) + it("should serialize ssv format query array parameter", async () => { + const result = await client + .path("/parameters/collection-format/query/ssv") + .get({ + queryParameters: { + colors: { + value: colors, + explode: false, + style: "spaceDelimited" } - }); - assert.strictEqual(result.status, "204"); - } catch (err) { - assert.fail(err as string); - } + } + }); + assert.strictEqual(result.status, "204"); }); it("should serialize pipes format query array parameter", async () => { - try { - const result = await client - .path("/parameters/collection-format/query/pipes") - .get({ - queryParameters: { - colors: buildPipeCollection(colors) - }, - skipUrlEncoding: true - }); - assert.strictEqual(result.status, "204"); - } catch (err) { - assert.fail(err as string); - } + const result = await client + .path("/parameters/collection-format/query/pipes") + .get({ + queryParameters: { + colors: { + value: colors, + explode: false, + style: "pipeDelimited" + } + } + }); + assert.strictEqual(result.status, "204"); }); it("should serialize csv format header array parameter", async () => { - try { - const result = await client - .path("/parameters/collection-format/header/csv") - .get({ - headers: { - colors: buildCsvCollection(colors) - }, - skipUrlEncoding: true - }); - assert.strictEqual(result.status, "204"); - } catch (err) { - assert.fail(err as string); - } + const result = await client + .path("/parameters/collection-format/header/csv") + .get({ + headers: { + colors: buildCsvCollection(colors) + }, + skipUrlEncoding: true + }); + assert.strictEqual(result.status, "204"); }); - - // it.skip("should serialize default format query array parameter", async () => { - // try { - // const result = await client.path("/collectionFormat/default").get({ - // queryParameters: { - // colors: buildMultiCollection(colors, 'colors') - // }, - // skipUrlEncoding: true - // }); - // assert.strictEqual(result.status, "200"); - // } catch (err) { - // assert.fail(err as string); - // } - // }); }); diff --git a/packages/typespec-ts/test/integration/generated/azure/core/basic/src/index.d.ts b/packages/typespec-ts/test/integration/generated/azure/core/basic/src/index.d.ts index 0375b794bd..6671cae2aa 100644 --- a/packages/typespec-ts/test/integration/generated/azure/core/basic/src/index.d.ts +++ b/packages/typespec-ts/test/integration/generated/azure/core/basic/src/index.d.ts @@ -15,8 +15,6 @@ export declare interface AzureCoreClientOptions extends ClientOptions { apiVersion?: string; } -export declare function buildMultiCollection(items: string[], parameterName: string): string; - declare function createClient({ apiVersion, ...options }?: AzureCoreClientOptions): AzureCoreClient; export default createClient; @@ -214,6 +212,18 @@ export declare interface ListDefaultResponse extends HttpResponse { headers: RawHttpHeaders & ListDefaultHeaders; } +export declare interface ListExpandQueryParam { + value: string[]; + explode: true; + style: "form"; +} + +export declare interface ListOrderbyQueryParam { + value: string[]; + explode: true; + style: "form"; +} + export declare type ListParameters = ListQueryParam & RequestParameters; export declare interface ListQueryParam { @@ -224,10 +234,16 @@ export declare interface ListQueryParamProperties { top?: number; skip?: number; maxpagesize?: number; - orderby?: string; + orderby?: ListOrderbyQueryParam; filter?: string; - select?: string; - expand?: string; + select?: ListSelectQueryParam; + expand?: ListExpandQueryParam; +} + +export declare interface ListSelectQueryParam { + value: string[]; + explode: true; + style: "form"; } export declare interface PagedAsyncIterableIterator { diff --git a/packages/typespec-ts/test/integration/generated/encode/bytes/src/index.d.ts b/packages/typespec-ts/test/integration/generated/encode/bytes/src/index.d.ts index 48da5b13dd..70207e090b 100644 --- a/packages/typespec-ts/test/integration/generated/encode/bytes/src/index.d.ts +++ b/packages/typespec-ts/test/integration/generated/encode/bytes/src/index.d.ts @@ -223,7 +223,13 @@ export declare interface QueryBase64urlArrayQueryParam { } export declare interface QueryBase64urlArrayQueryParamProperties { + value: string[] | QueryBase64urlArrayValueQueryParam; +} + +export declare interface QueryBase64urlArrayValueQueryParam { value: string[]; + explode: false; + style: "form"; } export declare type QueryBase64urlParameters = QueryBase64urlQueryParam & RequestParameters; diff --git a/packages/typespec-ts/test/integration/generated/encode/datetime/src/index.d.ts b/packages/typespec-ts/test/integration/generated/encode/datetime/src/index.d.ts index 92a3e51d07..42632262a2 100644 --- a/packages/typespec-ts/test/integration/generated/encode/datetime/src/index.d.ts +++ b/packages/typespec-ts/test/integration/generated/encode/datetime/src/index.d.ts @@ -268,7 +268,13 @@ export declare interface QueryUnixTimestampArrayQueryParam { } export declare interface QueryUnixTimestampArrayQueryParamProperties { + value: number[] | QueryUnixTimestampArrayValueQueryParam; +} + +export declare interface QueryUnixTimestampArrayValueQueryParam { value: number[]; + explode: false; + style: "form"; } export declare type QueryUnixTimestampParameters = QueryUnixTimestampQueryParam & RequestParameters; diff --git a/packages/typespec-ts/test/integration/generated/encode/duration/src/index.d.ts b/packages/typespec-ts/test/integration/generated/encode/duration/src/index.d.ts index 59c15bb80b..0d950809be 100644 --- a/packages/typespec-ts/test/integration/generated/encode/duration/src/index.d.ts +++ b/packages/typespec-ts/test/integration/generated/encode/duration/src/index.d.ts @@ -333,6 +333,12 @@ export declare interface QueryInt32SecondsArray204Response extends HttpResponse status: "204"; } +export declare interface QueryInt32SecondsArrayInputQueryParam { + value: number[]; + explode: false; + style: "form"; +} + export declare type QueryInt32SecondsArrayParameters = QueryInt32SecondsArrayQueryParam & RequestParameters; export declare interface QueryInt32SecondsArrayQueryParam { @@ -340,7 +346,7 @@ export declare interface QueryInt32SecondsArrayQueryParam { } export declare interface QueryInt32SecondsArrayQueryParamProperties { - input: number[]; + input: number[] | QueryInt32SecondsArrayInputQueryParam; } export declare type QueryInt32SecondsParameters = QueryInt32SecondsQueryParam & RequestParameters; diff --git a/packages/typespec-ts/test/integration/generated/parameters/collection-format/src/index.d.ts b/packages/typespec-ts/test/integration/generated/parameters/collection-format/src/index.d.ts index dbade79da4..e5613f094e 100644 --- a/packages/typespec-ts/test/integration/generated/parameters/collection-format/src/index.d.ts +++ b/packages/typespec-ts/test/integration/generated/parameters/collection-format/src/index.d.ts @@ -9,12 +9,6 @@ export declare function buildCsvCollection(items: string[] | number[]): string; export declare function buildMultiCollection(items: string[], parameterName: string): string; -export declare function buildPipeCollection(items: string[] | number[]): string; - -export declare function buildSsvCollection(items: string[] | number[]): string; - -export declare function buildTsvCollection(items: string[] | number[]): string; - export declare type CollectionFormatClient = Client & { path: Routes; }; @@ -51,6 +45,12 @@ export declare interface QueryCsv204Response extends HttpResponse { status: "204"; } +export declare interface QueryCsvColorsQueryParam { + value: string[]; + explode: false; + style: "form"; +} + export declare type QueryCsvParameters = QueryCsvQueryParam & RequestParameters; export declare interface QueryCsvQueryParam { @@ -58,7 +58,7 @@ export declare interface QueryCsvQueryParam { } export declare interface QueryCsvQueryParamProperties { - colors: string[]; + colors: string[] | QueryCsvColorsQueryParam; } export declare interface QueryMulti { @@ -69,6 +69,12 @@ export declare interface QueryMulti204Response extends HttpResponse { status: "204"; } +export declare interface QueryMultiColorsQueryParam { + value: string[]; + explode: true; + style: "form"; +} + export declare type QueryMultiParameters = QueryMultiQueryParam & RequestParameters; export declare interface QueryMultiQueryParam { @@ -76,7 +82,7 @@ export declare interface QueryMultiQueryParam { } export declare interface QueryMultiQueryParamProperties { - colors: string; + colors: QueryMultiColorsQueryParam | string; } export declare interface QueryPipes { @@ -87,6 +93,12 @@ export declare interface QueryPipes204Response extends HttpResponse { status: "204"; } +export declare interface QueryPipesColorsQueryParam { + value: string[]; + explode: false; + style: "pipeDelimited"; +} + export declare type QueryPipesParameters = QueryPipesQueryParam & RequestParameters; export declare interface QueryPipesQueryParam { @@ -94,7 +106,7 @@ export declare interface QueryPipesQueryParam { } export declare interface QueryPipesQueryParamProperties { - colors: string; + colors: QueryPipesColorsQueryParam; } export declare interface QuerySsv { @@ -105,6 +117,12 @@ export declare interface QuerySsv204Response extends HttpResponse { status: "204"; } +export declare interface QuerySsvColorsQueryParam { + value: string[]; + explode: false; + style: "spaceDelimited"; +} + export declare type QuerySsvParameters = QuerySsvQueryParam & RequestParameters; export declare interface QuerySsvQueryParam { @@ -112,7 +130,7 @@ export declare interface QuerySsvQueryParam { } export declare interface QuerySsvQueryParamProperties { - colors: string; + colors: QuerySsvColorsQueryParam; } export declare interface QueryTsv { diff --git a/packages/typespec-ts/test/integration/generated/parameters/collection-format/tspconfig.yaml b/packages/typespec-ts/test/integration/generated/parameters/collection-format/tspconfig.yaml index 283b30b3ab..25e4be7579 100644 --- a/packages/typespec-ts/test/integration/generated/parameters/collection-format/tspconfig.yaml +++ b/packages/typespec-ts/test/integration/generated/parameters/collection-format/tspconfig.yaml @@ -8,6 +8,7 @@ options: addCredentials: false azureSdkForJs: false enableOperationGroup: true + compatibilityQueryMultiFormat: true isTypeSpecTest: true title: CollectionFormatClient packageDetails: diff --git a/packages/typespec-ts/test/integration/generated/routes/src/index.d.ts b/packages/typespec-ts/test/integration/generated/routes/src/index.d.ts index 1df127f10d..195a8fe287 100644 --- a/packages/typespec-ts/test/integration/generated/routes/src/index.d.ts +++ b/packages/typespec-ts/test/integration/generated/routes/src/index.d.ts @@ -237,6 +237,11 @@ export declare interface PathParametersReservedExpansionAnnotation204Response ex export declare type PathParametersReservedExpansionAnnotationParameters = RequestParameters; +export declare interface PathParametersReservedExpansionAnnotationParamPathParam { + value: string; + allowReserved: true; +} + export declare interface PathParametersReservedExpansionTemplate { get(options?: PathParametersReservedExpansionTemplateParameters): StreamableMethod; } @@ -247,6 +252,11 @@ export declare interface PathParametersReservedExpansionTemplate204Response exte export declare type PathParametersReservedExpansionTemplateParameters = RequestParameters; +export declare interface PathParametersReservedExpansionTemplateParamPathParam { + value: string; + allowReserved: true; +} + export declare interface PathParametersSimpleExpansionExplodeArray { get(options?: PathParametersSimpleExpansionExplodeArrayParameters): StreamableMethod; } @@ -363,12 +373,18 @@ export declare interface QueryParametersQueryContinuationExplodeArray204Response export declare type QueryParametersQueryContinuationExplodeArrayParameters = QueryParametersQueryContinuationExplodeArrayQueryParam & RequestParameters; +export declare interface QueryParametersQueryContinuationExplodeArrayParamQueryParam { + value: string[]; + explode: true; + style: "form"; +} + export declare interface QueryParametersQueryContinuationExplodeArrayQueryParam { queryParameters: QueryParametersQueryContinuationExplodeArrayQueryParamProperties; } export declare interface QueryParametersQueryContinuationExplodeArrayQueryParamProperties { - param: string[]; + param: QueryParametersQueryContinuationExplodeArrayParamQueryParam; } export declare interface QueryParametersQueryContinuationExplodePrimitive { @@ -399,12 +415,18 @@ export declare interface QueryParametersQueryContinuationExplodeRecord204Respons export declare type QueryParametersQueryContinuationExplodeRecordParameters = QueryParametersQueryContinuationExplodeRecordQueryParam & RequestParameters; +export declare interface QueryParametersQueryContinuationExplodeRecordParamQueryParam { + value: Record; + explode: true; + style: "form"; +} + export declare interface QueryParametersQueryContinuationExplodeRecordQueryParam { queryParameters: QueryParametersQueryContinuationExplodeRecordQueryParamProperties; } export declare interface QueryParametersQueryContinuationExplodeRecordQueryParamProperties { - param: Record; + param: QueryParametersQueryContinuationExplodeRecordParamQueryParam; } export declare interface QueryParametersQueryContinuationStandardArray { @@ -417,12 +439,18 @@ export declare interface QueryParametersQueryContinuationStandardArray204Respons export declare type QueryParametersQueryContinuationStandardArrayParameters = QueryParametersQueryContinuationStandardArrayQueryParam & RequestParameters; +export declare interface QueryParametersQueryContinuationStandardArrayParamQueryParam { + value: string[]; + explode: false; + style: "form"; +} + export declare interface QueryParametersQueryContinuationStandardArrayQueryParam { queryParameters: QueryParametersQueryContinuationStandardArrayQueryParamProperties; } export declare interface QueryParametersQueryContinuationStandardArrayQueryParamProperties { - param: string[]; + param: string[] | QueryParametersQueryContinuationStandardArrayParamQueryParam; } export declare interface QueryParametersQueryContinuationStandardPrimitive { @@ -453,12 +481,18 @@ export declare interface QueryParametersQueryContinuationStandardRecord204Respon export declare type QueryParametersQueryContinuationStandardRecordParameters = QueryParametersQueryContinuationStandardRecordQueryParam & RequestParameters; +export declare interface QueryParametersQueryContinuationStandardRecordParamQueryParam { + value: Record; + explode: false; + style: "form"; +} + export declare interface QueryParametersQueryContinuationStandardRecordQueryParam { queryParameters: QueryParametersQueryContinuationStandardRecordQueryParamProperties; } export declare interface QueryParametersQueryContinuationStandardRecordQueryParamProperties { - param: Record; + param: QueryParametersQueryContinuationStandardRecordParamQueryParam; } export declare interface QueryParametersQueryExpansionExplodeArray { @@ -471,12 +505,18 @@ export declare interface QueryParametersQueryExpansionExplodeArray204Response ex export declare type QueryParametersQueryExpansionExplodeArrayParameters = QueryParametersQueryExpansionExplodeArrayQueryParam & RequestParameters; +export declare interface QueryParametersQueryExpansionExplodeArrayParamQueryParam { + value: string[]; + explode: true; + style: "form"; +} + export declare interface QueryParametersQueryExpansionExplodeArrayQueryParam { queryParameters: QueryParametersQueryExpansionExplodeArrayQueryParamProperties; } export declare interface QueryParametersQueryExpansionExplodeArrayQueryParamProperties { - param: string[]; + param: QueryParametersQueryExpansionExplodeArrayParamQueryParam; } export declare interface QueryParametersQueryExpansionExplodePrimitive { @@ -507,12 +547,18 @@ export declare interface QueryParametersQueryExpansionExplodeRecord204Response e export declare type QueryParametersQueryExpansionExplodeRecordParameters = QueryParametersQueryExpansionExplodeRecordQueryParam & RequestParameters; +export declare interface QueryParametersQueryExpansionExplodeRecordParamQueryParam { + value: Record; + explode: true; + style: "form"; +} + export declare interface QueryParametersQueryExpansionExplodeRecordQueryParam { queryParameters: QueryParametersQueryExpansionExplodeRecordQueryParamProperties; } export declare interface QueryParametersQueryExpansionExplodeRecordQueryParamProperties { - param: Record; + param: QueryParametersQueryExpansionExplodeRecordParamQueryParam; } export declare interface QueryParametersQueryExpansionStandardArray { @@ -525,12 +571,18 @@ export declare interface QueryParametersQueryExpansionStandardArray204Response e export declare type QueryParametersQueryExpansionStandardArrayParameters = QueryParametersQueryExpansionStandardArrayQueryParam & RequestParameters; +export declare interface QueryParametersQueryExpansionStandardArrayParamQueryParam { + value: string[]; + explode: false; + style: "form"; +} + export declare interface QueryParametersQueryExpansionStandardArrayQueryParam { queryParameters: QueryParametersQueryExpansionStandardArrayQueryParamProperties; } export declare interface QueryParametersQueryExpansionStandardArrayQueryParamProperties { - param: string[]; + param: string[] | QueryParametersQueryExpansionStandardArrayParamQueryParam; } export declare interface QueryParametersQueryExpansionStandardPrimitive { @@ -561,12 +613,18 @@ export declare interface QueryParametersQueryExpansionStandardRecord204Response export declare type QueryParametersQueryExpansionStandardRecordParameters = QueryParametersQueryExpansionStandardRecordQueryParam & RequestParameters; +export declare interface QueryParametersQueryExpansionStandardRecordParamQueryParam { + value: Record; + explode: false; + style: "form"; +} + export declare interface QueryParametersQueryExpansionStandardRecordQueryParam { queryParameters: QueryParametersQueryExpansionStandardRecordQueryParamProperties; } export declare interface QueryParametersQueryExpansionStandardRecordQueryParamProperties { - param: Record; + param: QueryParametersQueryExpansionStandardRecordParamQueryParam; } export declare interface QueryParametersTemplateOnly { @@ -592,8 +650,8 @@ export declare interface Routes { (path: "/routes/path/template-only/{param}", param: string): PathParametersTemplateOnly; (path: "/routes/path/explicit/{param}", param: string): PathParametersExplicit; (path: "/routes/path/annotation-only/{param}", param: string): PathParametersAnnotationOnly; - (path: "/routes/path/reserved-expansion/template/{param}", param: string): PathParametersReservedExpansionTemplate; - (path: "/routes/path/reserved-expansion/annotation/{param}", param: string): PathParametersReservedExpansionAnnotation; + (path: "/routes/path/reserved-expansion/template/{param}", param: PathParametersReservedExpansionTemplateParamPathParam): PathParametersReservedExpansionTemplate; + (path: "/routes/path/reserved-expansion/annotation/{param}", param: PathParametersReservedExpansionAnnotationParamPathParam): PathParametersReservedExpansionAnnotation; (path: "/routes/path/simple/standard/primitive{param}", param: string): PathParametersSimpleExpansionStandardPrimitive; (path: "/routes/path/simple/standard/array{param}", param: string[]): PathParametersSimpleExpansionStandardArray; (path: "/routes/path/simple/standard/record{param}", param: Record): PathParametersSimpleExpansionStandardRecord; diff --git a/packages/typespec-ts/test/integration/generated/routes/tspconfig.yaml b/packages/typespec-ts/test/integration/generated/routes/tspconfig.yaml index d033c76ee1..ba69ca1b18 100644 --- a/packages/typespec-ts/test/integration/generated/routes/tspconfig.yaml +++ b/packages/typespec-ts/test/integration/generated/routes/tspconfig.yaml @@ -7,7 +7,6 @@ options: addCredentials: false azureSdkForJs: false isTypeSpecTest: true - generateSample: true title: RoutesClient packageDetails: name: "@msinternal/routes" diff --git a/packages/typespec-ts/test/integration/routes.spec.ts b/packages/typespec-ts/test/integration/routes.spec.ts index 852696d05e..dbfd001b9f 100644 --- a/packages/typespec-ts/test/integration/routes.spec.ts +++ b/packages/typespec-ts/test/integration/routes.spec.ts @@ -1,5 +1,5 @@ import RoutesClientFactory, { - RoutesClient + RoutesClient, } from "./generated/routes/src/index.js"; import { assert } from "chai"; describe("RoutesClient Rest Client", () => { @@ -32,13 +32,107 @@ describe("RoutesClient Rest Client", () => { assert.strictEqual(result.status, "204"); }); - it.skip("should have PathParameters SimpleExpansion Standard primitive", async () => { + it("should have allowReserved: true", async () => { const result = await client - .path("/routes/path/simple/standard/primitive{param}", "a") + .path("/routes/path/reserved-expansion/template/{param}", { + value: "foo/bar baz", + allowReserved: true + }) .get(); assert.strictEqual(result.status, "204"); }); + it("should have allowReserved: true with helper", async () => { + const result = await client + .path( + "/routes/path/reserved-expansion/template/{param}", + { + value: "foo/bar baz", + allowReserved: true + } + ) + .get(); + assert.strictEqual(result.status, "204"); + }); + + it("should have explode: true array", async () => { + const result = await client + .path("/routes/query/query-expansion/explode/array") + .get({ + queryParameters: { + param: { + value: ["a", "b"], + explode: true, + style: "form" + } + } + }); + assert.strictEqual(result.status, "204"); + }); + + it("should have explode: true record", async () => { + const result = await client + .path("/routes/query/query-expansion/explode/record") + .get({ + queryParameters: { + param: { + value: { a: 1, b: 2 }, + explode: true, + style: "form" + } + } + }); + assert.strictEqual(result.status, "204"); + }); + + it("should have explode: true primitive", async () => { + const result = await client + .path("/routes/query/query-expansion/explode/primitive") + .get({ + queryParameters: { + param: "a" + } + }); + assert.strictEqual(result.status, "204"); + }); + + it("should have explode: false array", async () => { + const result = await client + .path("/routes/query/query-expansion/standard/array") + .get({ + queryParameters: { + param: ["a", "b"] + } + }); + assert.strictEqual(result.status, "204"); + }); + + it("should have explode: false record", async () => { + const result = await client + .path("/routes/query/query-expansion/standard/record") + .get({ + queryParameters: { + param: { + value: { a: 1, b: 2 }, + explode: false, + style: "form" + } + } + }); + assert.strictEqual(result.status, "204"); + }); + + it("should have explode: false primitive", async () => { + const result = await client + .path("/routes/query/query-expansion/standard/primitive") + .get({ + queryParameters: { + param: "a" + } + }); + assert.strictEqual(result.status, "204"); + }); + it("should have QueryParameters templateOnly", async () => { const result = await client .path("/routes/query/template-only") @@ -59,4 +153,86 @@ describe("RoutesClient Rest Client", () => { .get({ queryParameters: { param: "a" } }); assert.strictEqual(result.status, "204"); }); + + describe("Query continuation", () => { + it("should pass query-continuation with standard array correctly", async () => { + const result = await client + .path("/routes/query/query-continuation/standard/array?fixed=true") + .get({ + queryParameters: { + param: ["a", "b"] + } + }); + assert.strictEqual(result.status, "204"); + }); + + + it("should pass query-continuation with standard primitive correctly", async () => { + const result = await client + .path("/routes/query/query-continuation/standard/primitive?fixed=true") + .get({ + queryParameters: { + param: "a" + } + }); + assert.strictEqual(result.status, "204"); + }); + + it("should pass query-continuation with standard record correctly", async () => { + const result = await client + .path("/routes/query/query-continuation/standard/record?fixed=true") + .get({ + queryParameters: { + param: { + value: { a: 1, b: 2 }, + explode: false, + style: "form" + } + } + }); + assert.strictEqual(result.status, "204"); + }); + + it("should pass query-continuation with exploded record correctly", async () => { + const result = await client + .path("/routes/query/query-continuation/explode/record?fixed=true") + .get({ + queryParameters: { + param: { + value: { a: 1, b: 2 }, + explode: true, + style: "form" + } + } + }); + assert.strictEqual(result.status, "204"); + }); + + it("should pass query-continuation with exploded primitive correctly", async () => { + const result = await client + .path("/routes/query/query-continuation/explode/primitive?fixed=true") + .get({ + queryParameters: { + param: "a" + } + }); + assert.strictEqual(result.status, "204"); + }); + + it("should pass query-continuation with exploded array correctly", async () => { + const result = await client + .path("/routes/query/query-continuation/explode/array?fixed=true") + .get({ + queryParameters: { + param: { + value: ["a", "b"], + explode: true, + style: "form" + } + } + }); + assert.strictEqual(result.status, "204"); + }); + }); + }); diff --git a/packages/typespec-ts/test/modularUnit/operations.spec.ts b/packages/typespec-ts/test/modularUnit/operations.spec.ts index 290d5eca31..899f7d1b8d 100644 --- a/packages/typespec-ts/test/modularUnit/operations.spec.ts +++ b/packages/typespec-ts/test/modularUnit/operations.spec.ts @@ -121,8 +121,12 @@ describe("operations", () => { ...Bar): OkResponse; `; - const operationFiles = - await emitModularOperationsFromTypeSpec(tspContent); + const operationFiles = await emitModularOperationsFromTypeSpec( + tspContent, + { + mustEmptyDiagnostic: false + } + ); assert.ok(operationFiles); assert.equal(operationFiles?.length, 1); await assertEqualContent( diff --git a/packages/typespec-ts/test/unit/uriTemplate.spec.ts b/packages/typespec-ts/test/unit/uriTemplate.spec.ts new file mode 100644 index 0000000000..aaa011815c --- /dev/null +++ b/packages/typespec-ts/test/unit/uriTemplate.spec.ts @@ -0,0 +1,57 @@ +import { assert } from "chai"; +import { emitClientDefinitionFromTypeSpec, emitParameterFromTypeSpec } from "../util/emitUtil.js"; +import { assertEqualContent } from "../util/testUtil.js"; + +describe("Client definition generation", () => { + it("should generate method-level parameter", async () => { + const tsp = ` + @route("template/{+param}") + op template(param: string): void; + `; + const parameters = await emitParameterFromTypeSpec( + tsp + ); + assert.ok(parameters); + await assertEqualContent( + parameters?.content!, + ` + import { RequestParameters } from "@azure-rest/core-client"; + + /** This is the wrapper object for the parameter \`param\` with allowReserved set to true. */ + export interface TemplateParamPathParam { + /** A sequence of textual characters. */ + value: string; + /** Whether to allow reserved characters */ + allowReserved: true; + } + + export type TemplateParameters = RequestParameters; + ` + ); + + const clientDef = await emitClientDefinitionFromTypeSpec( + tsp); + assert.ok(clientDef); + await assertEqualContent( + clientDef!.content, + ` + import { TemplateParameters, TemplateParamPathParam } from "./parameters.js"; + import { Template204Response } from "./responses.js"; + import { Client, StreamableMethod } from "@azure-rest/core-client"; + + export interface Template { + get(options?: TemplateParameters): StreamableMethod; + } + + export interface Routes { + /** Resource for '/template/\\{param\\}' has methods for the following verbs: get */ + (path: "/template/{param}", param: TemplateParamPathParam): Template; + } + + export type testClient = Client & { + path: Routes; + }; + ` + ); + }); +}); diff --git a/packages/typespec-ts/test/util/emitUtil.ts b/packages/typespec-ts/test/util/emitUtil.ts index 68b00f0fc3..f0f593ab4f 100644 --- a/packages/typespec-ts/test/util/emitUtil.ts +++ b/packages/typespec-ts/test/util/emitUtil.ts @@ -1,4 +1,5 @@ import { + OperationParameter, RLCModel, Schema, buildClient, @@ -190,6 +191,7 @@ export async function emitParameterFromTypeSpec( const clients = getRLCClients(dpgContext); const importSet = initInternalImports(); let parameters; + let helperDetails; if (clients && clients[0]) { const urlInfo = transformUrlInfo(clients[0], dpgContext, importSet); parameters = transformToParameterTypes( @@ -198,6 +200,7 @@ export async function emitParameterFromTypeSpec( importSet, urlInfo?.apiVersionInfo ); + helperDetails = transformHelperFunctionDetails(clients[0], dpgContext); } if (mustEmptyDiagnostic && dpgContext.program.diagnostics.length > 0) { throw dpgContext.program.diagnostics; @@ -208,9 +211,13 @@ export async function emitParameterFromTypeSpec( libraryName: "test", schemas: [], parameters, + helperDetails, importInfo: { internalImports: importSet, runtimeImports: buildRuntimeImports("azure") + }, + options: { + sourceFrom: "TypeSpec" } }); } @@ -231,8 +238,14 @@ export async function emitClientDefinitionFromTypeSpec( const clients = getRLCClients(dpgContext); const internalImports = initInternalImports(); let paths = {}; + let parameters: OperationParameter[] = []; if (clients && clients[0]) { paths = transformPaths(clients[0], dpgContext, internalImports); + parameters = transformToParameterTypes( + clients[0], + dpgContext, + internalImports + ); } expectDiagnosticEmpty(dpgContext.program.diagnostics); return buildClientDefinitions({ @@ -240,6 +253,7 @@ export async function emitClientDefinitionFromTypeSpec( libraryName: "test", schemas: [], paths, + parameters, importInfo: { internalImports, runtimeImports: buildRuntimeImports("azure")