diff --git a/.chronus/changes/multipart-bodyparam-fix-2024-2024-7-1-15-10-38.md b/.chronus/changes/multipart-bodyparam-fix-2024-2024-7-1-15-10-38.md new file mode 100644 index 0000000000..61e8de0ff5 --- /dev/null +++ b/.chronus/changes/multipart-bodyparam-fix-2024-2024-7-1-15-10-38.md @@ -0,0 +1,7 @@ +--- +changeKind: fix +packages: + - "@azure-tools/typespec-client-generator-core" +--- + +Support @multipartBody for `bodyParam` of `SdkHttpOperation` \ No newline at end of file diff --git a/packages/typespec-client-generator-core/src/http.ts b/packages/typespec-client-generator-core/src/http.ts index 07ea45cba5..81b3cb6d3a 100644 --- a/packages/typespec-client-generator-core/src/http.ts +++ b/packages/typespec-client-generator-core/src/http.ts @@ -127,7 +127,7 @@ function getSdkHttpParameters( const tspBody = httpOperation.parameters.body; // we add correspondingMethodParams after we create the type, since we need the info on the type const correspondingMethodParams: SdkModelPropertyType[] = []; - if (tspBody && tspBody?.bodyKind !== "multipart") { + if (tspBody) { // if there's a param on the body, we can just rely on getSdkHttpParameter if (tspBody.property && !isNeverOrVoidType(tspBody.property.type)) { const getParamResponse = diagnostics.pipe( diff --git a/packages/typespec-client-generator-core/src/types.ts b/packages/typespec-client-generator-core/src/types.ts index 0ed24c6da3..d99eb246b0 100644 --- a/packages/typespec-client-generator-core/src/types.ts +++ b/packages/typespec-client-generator-core/src/types.ts @@ -1336,7 +1336,15 @@ export function getSdkModelPropertyType( flatten: shouldFlattenProperty(context, type), }; if (operation) { - diagnostics.pipe(updateMultiPartInfo(context, type, result, operation)); + const httpOperation = getHttpOperationWithCache(context, operation); + if ( + type.model && + httpOperation.parameters.body && + httpOperation.parameters.body.type === type.model + ) { + // only add multipartOptions for property of multipart body + diagnostics.pipe(updateMultiPartInfo(context, type, result, operation)); + } } return diagnostics.wrap(result); } diff --git a/packages/typespec-client-generator-core/test/package.test.ts b/packages/typespec-client-generator-core/test/package.test.ts index 9987fee7a4..666ea72ea3 100644 --- a/packages/typespec-client-generator-core/test/package.test.ts +++ b/packages/typespec-client-generator-core/test/package.test.ts @@ -3111,6 +3111,14 @@ describe("typespec-client-generator-core: package", () => { strictEqual(op.bodyParam.kind, "body"); strictEqual(op.bodyParam.name, "testRequest"); deepStrictEqual(op.bodyParam.correspondingMethodParams, [documentMethodParam]); + + const anonymousModel = runner.context.sdkPackage.models[0]; + strictEqual(anonymousModel.properties.length, 1); + strictEqual(anonymousModel.properties[0].kind, "property"); + strictEqual(anonymousModel.properties[0].isMultipartFileInput, true); + ok(anonymousModel.properties[0].multipartOptions); + strictEqual(anonymousModel.properties[0].multipartOptions.isFilePart, true); + strictEqual(anonymousModel.properties[0].multipartOptions.isMulti, false); }); it("anonymous model with @body should not be spread", async () => { diff --git a/packages/typespec-client-generator-core/test/types/model-types.test.ts b/packages/typespec-client-generator-core/test/types/model-types.test.ts index 7574650c58..177976849c 100644 --- a/packages/typespec-client-generator-core/test/types/model-types.test.ts +++ b/packages/typespec-client-generator-core/test/types/model-types.test.ts @@ -1502,4 +1502,71 @@ describe("typespec-client-generator-core: model types", () => { ok(inputModel); strictEqual(inputModel.usage, UsageFlags.Input | UsageFlags.Xml); }); + + it("check bodyParam for @multipartBody", async function () { + await runner.compileWithBuiltInService(` + model Address { + city: string; + } + model MultiPartRequest{ + id?: HttpPart; + profileImage: HttpPart; + address: HttpPart
; + picture: HttpPart; + } + @post + op upload(@header contentType: "multipart/form-data", @multipartBody body: MultiPartRequest): void; + `); + const formDataMethod = runner.context.sdkPackage.clients[0].methods[0]; + strictEqual(formDataMethod.kind, "basic"); + strictEqual(formDataMethod.name, "upload"); + strictEqual(formDataMethod.parameters.length, 2); + + strictEqual(formDataMethod.parameters[0].name, "contentType"); + strictEqual(formDataMethod.parameters[0].type.kind, "constant"); + strictEqual(formDataMethod.parameters[0].type.value, "multipart/form-data"); + + strictEqual(formDataMethod.parameters[1].name, "body"); + strictEqual(formDataMethod.parameters[1].type.kind, "model"); + strictEqual(formDataMethod.parameters[1].type.name, "MultiPartRequest"); + + const formDataOp = formDataMethod.operation; + strictEqual(formDataOp.parameters.length, 1); + ok(formDataOp.parameters.find((x) => x.name === "contentType" && x.kind === "header")); + + const formDataBodyParam = formDataOp.bodyParam; + ok(formDataBodyParam); + strictEqual(formDataBodyParam.type.kind, "model"); + strictEqual(formDataBodyParam.type.name, "MultiPartRequest"); + strictEqual(formDataBodyParam.correspondingMethodParams.length, 1); + strictEqual(formDataBodyParam.type.properties.length, 4); + strictEqual(formDataBodyParam.type.properties[0].name, "id"); + strictEqual(formDataBodyParam.type.properties[0].type.kind, "string"); + strictEqual(formDataBodyParam.type.properties[1].name, "profileImage"); + strictEqual(formDataBodyParam.type.properties[1].type.kind, "bytes"); + strictEqual(formDataBodyParam.type.properties[2].name, "address"); + strictEqual(formDataBodyParam.type.properties[2].type.kind, "model"); + strictEqual(formDataBodyParam.type.properties[2].type.name, "Address"); + strictEqual(formDataBodyParam.type.properties[3].name, "picture"); + strictEqual(formDataBodyParam.type.properties[3].type.kind, "model"); + strictEqual(formDataBodyParam.type.properties[3].type.name, "File"); + }); + + it("check multipartOptions for property of base model", async function () { + await runner.compileWithBuiltInService(` + model MultiPartRequest{ + fileProperty: HttpPart; + } + @post + op upload(@header contentType: "multipart/form-data", @multipartBody body: MultiPartRequest): void; + `); + const models = runner.context.sdkPackage.models; + const fileModel = models.find((x) => x.name === "File"); + ok(fileModel); + for (const p of fileModel.properties) { + strictEqual(p.kind, "property"); + strictEqual(p.isMultipartFileInput, false); + strictEqual(p.multipartOptions, undefined); + } + }); });