diff --git a/.chronus/changes/http-client-java_support-file-2026-1-6-12-16-2.md b/.chronus/changes/http-client-java_support-file-2026-1-6-12-16-2.md new file mode 100644 index 00000000000..20e82fabf5e --- /dev/null +++ b/.chronus/changes/http-client-java_support-file-2026-1-6-12-16-2.md @@ -0,0 +1,7 @@ +--- +changeKind: feature +packages: + - "@typespec/http-client-java" +--- + +Support File in multipart and request body. \ No newline at end of file diff --git a/packages/http-client-java/emitter/src/code-model-builder.ts b/packages/http-client-java/emitter/src/code-model-builder.ts index b44735600d2..fde3af16885 100644 --- a/packages/http-client-java/emitter/src/code-model-builder.ts +++ b/packages/http-client-java/emitter/src/code-model-builder.ts @@ -1982,16 +1982,28 @@ export class CodeModelBuilder { // set contentTypes to mediaTypes op.requests![0].protocol.http!.mediaTypes = sdkBody.contentTypes; - const unknownRequestBody = - op.requests![0].protocol.http!.mediaTypes && - op.requests![0].protocol.http!.mediaTypes.length > 0 && - !isKnownContentType(op.requests![0].protocol.http!.mediaTypes); - const sdkType: SdkType = sdkBody.type; + let requestBodyIsFile: boolean = false; + if ( + sdkType && + sdkType.kind === "model" && + sdkType.serializationOptions.binary && + sdkType.serializationOptions.binary.isFile + ) { + // check for File + requestBodyIsFile = true; + } else if (sdkType && sdkType.kind === "bytes") { + // check for bytes + unknown content-type + const mediaTypes = op.requests![0].protocol.http!.mediaTypes; + const unknownRequestBody = + mediaTypes && mediaTypes.length > 0 && !isKnownContentType(mediaTypes); + requestBodyIsFile = Boolean(unknownRequestBody); + } + let schema: Schema; - if (unknownRequestBody && sdkType.kind === "bytes") { - // if it's unknown request body, handle binary request body + if (requestBodyIsFile) { + // binary/file schema = this.processBinarySchema(sdkType); } else { schema = this.processSchema(getNonNullSdkType(sdkType), sdkBody.name); @@ -2226,7 +2238,7 @@ export class CodeModelBuilder { const bodyType: SdkType | undefined = sdkResponse.type; let trackConvenienceApi: boolean = Boolean(op.convenienceApi); - let responseIsFile: boolean = false; + let responseBodyIsFile: boolean = false; if ( bodyType && bodyType.kind === "model" && @@ -2234,18 +2246,18 @@ export class CodeModelBuilder { bodyType.serializationOptions.binary.isFile ) { // check for File - responseIsFile = true; + responseBodyIsFile = true; } else if (bodyType && bodyType.kind === "bytes") { // check for bytes + unknown content-type const unknownResponseBody = sdkResponse.contentTypes && sdkResponse.contentTypes.length > 0 && !isKnownContentType(sdkResponse.contentTypes); - responseIsFile = Boolean(unknownResponseBody); + responseBodyIsFile = Boolean(unknownResponseBody); } let response: Response; - if (responseIsFile) { + if (responseBodyIsFile) { // binary/file response = new BinaryResponse({ protocol: { @@ -2978,7 +2990,7 @@ export class CodeModelBuilder { return this.codeModel.schemas.add(unionSchema); } - private processBinarySchema(type: SdkBuiltInType): BinarySchema { + private processBinarySchema(type: SdkType): BinarySchema { return this.codeModel.schemas.add( new BinarySchema(type.doc ?? "", { summary: type.summary, diff --git a/packages/http-client-java/emitter/src/external-schemas.ts b/packages/http-client-java/emitter/src/external-schemas.ts index a4506b0a53d..df28619749d 100644 --- a/packages/http-client-java/emitter/src/external-schemas.ts +++ b/packages/http-client-java/emitter/src/external-schemas.ts @@ -194,7 +194,7 @@ const fileDetailsMap: Map = new Map(); function getFileSchemaName(baseName: string, sdkModelType?: SdkModelType): string { // If the TypeSpec Model exists and is not TypeSpec.Http.File, directly use its name if (sdkModelType && sdkModelType.crossLanguageDefinitionId !== "TypeSpec.Http.File") { - return baseName; + return sdkModelType.name; } // make sure suffix "FileDetails" @@ -250,17 +250,21 @@ function addFilenameProperty( filenameProperty?: SdkModelPropertyType, processSchemaFunc?: (type: SdkType) => Schema, ) { + const isRequired = filenameProperty ? !filenameProperty.optional : false; + const isConstant = filenameProperty?.type.kind === "constant" && isRequired; + // If the type is constant but not required, treat the type as non-constant String but its value as the default. + const clientDefaultValue = + filenameProperty?.type.kind === "constant" ? String(filenameProperty.type.value) : undefined; fileDetailsSchema.addProperty( new Property( "filename", "The filename of the file.", - filenameProperty?.type.kind === "constant" && processSchemaFunc - ? processSchemaFunc(filenameProperty.type) - : stringSchema, + isConstant && processSchemaFunc ? processSchemaFunc(filenameProperty.type) : stringSchema, { - required: filenameProperty ? !filenameProperty.optional : false, + required: isRequired, nullable: false, readOnly: false, + clientDefaultValue: clientDefaultValue, }, ), ); @@ -272,19 +276,27 @@ function addContentTypeProperty( contentTypeProperty?: SdkModelPropertyType, processSchemaFunc?: (type: SdkType) => Schema, ) { + const isRequired = contentTypeProperty ? !contentTypeProperty.optional : false; + const isConstant = contentTypeProperty?.type.kind === "constant" && isRequired; + // If the type is constant but not required, treat the type as non-constant String but its value as the default. + /* + * TypeSpec 'TypeSpec.Http.File<"image/png">' is such case. + * Feels that it is not user-friendly to create a single value enum for FileContentType that user probably had to set for the request. + */ + const clientDefaultValue = + contentTypeProperty?.type.kind === "constant" + ? String(contentTypeProperty.type.value) + : "application/octet-stream"; fileDetailsSchema.addProperty( new Property( "contentType", "The content-type of the file.", - contentTypeProperty?.type.kind === "constant" && processSchemaFunc - ? processSchemaFunc(contentTypeProperty.type) - : stringSchema, + isConstant && processSchemaFunc ? processSchemaFunc(contentTypeProperty.type) : stringSchema, { - required: contentTypeProperty ? !contentTypeProperty.optional : false, + required: isRequired, nullable: false, readOnly: false, - clientDefaultValue: - contentTypeProperty?.type.kind === "constant" ? undefined : "application/octet-stream", + clientDefaultValue: clientDefaultValue, }, ), ); @@ -317,8 +329,7 @@ export function getFileDetailsSchema( - Allow required for "filename" and "contentType" */ const filePropertyName = property.name; - const fileSchemaName = fileSdkType.name; - const schemaName = getFileSchemaName(fileSchemaName, fileSdkType); + const schemaName = getFileSchemaName(filePropertyName, fileSdkType); let fileDetailsSchema = fileDetailsMap.get(schemaName); if (!fileDetailsSchema) { const typeNamespace = getNamespace(property.type.__raw) ?? namespace; diff --git a/packages/http-client-java/generator/http-client-generator-test/src/main/java/tsptest/multipart/models/FileDetails.java b/packages/http-client-java/generator/http-client-generator-test/src/main/java/tsptest/multipart/models/FileData2FileDetails.java similarity index 91% rename from packages/http-client-java/generator/http-client-generator-test/src/main/java/tsptest/multipart/models/FileDetails.java rename to packages/http-client-java/generator/http-client-generator-test/src/main/java/tsptest/multipart/models/FileData2FileDetails.java index 63e0e2933b5..44b6882fe13 100644 --- a/packages/http-client-java/generator/http-client-generator-test/src/main/java/tsptest/multipart/models/FileDetails.java +++ b/packages/http-client-java/generator/http-client-generator-test/src/main/java/tsptest/multipart/models/FileData2FileDetails.java @@ -52,7 +52,7 @@ * is overridden, as shown in the examples below. */ @Fluent -public final class FileDetails { +public final class FileData2FileDetails { /* * The content of the file. */ @@ -72,12 +72,12 @@ public final class FileDetails { private String contentType = "application/octet-stream"; /** - * Creates an instance of FileDetails class. + * Creates an instance of FileData2FileDetails class. * * @param content the content value to set. */ @Generated - public FileDetails(BinaryData content) { + public FileData2FileDetails(BinaryData content) { this.content = content; } @@ -105,10 +105,10 @@ public String getFilename() { * Set the filename property: The filename of the file. * * @param filename the filename value to set. - * @return the FileDetails object itself. + * @return the FileData2FileDetails object itself. */ @Generated - public FileDetails setFilename(String filename) { + public FileData2FileDetails setFilename(String filename) { this.filename = filename; return this; } @@ -127,10 +127,10 @@ public String getContentType() { * Set the contentType property: The content-type of the file. * * @param contentType the contentType value to set. - * @return the FileDetails object itself. + * @return the FileData2FileDetails object itself. */ @Generated - public FileDetails setContentType(String contentType) { + public FileData2FileDetails setContentType(String contentType) { this.contentType = contentType; return this; } diff --git a/packages/http-client-java/generator/http-client-generator-test/src/main/java/tsptest/multipart/models/UploadHttpPartRequest.java b/packages/http-client-java/generator/http-client-generator-test/src/main/java/tsptest/multipart/models/UploadHttpPartRequest.java index 69f7bcdee36..1ded3050a85 100644 --- a/packages/http-client-java/generator/http-client-generator-test/src/main/java/tsptest/multipart/models/UploadHttpPartRequest.java +++ b/packages/http-client-java/generator/http-client-generator-test/src/main/java/tsptest/multipart/models/UploadHttpPartRequest.java @@ -22,7 +22,7 @@ public final class UploadHttpPartRequest { * The file_data2 property. */ @Generated - private final FileDetails fileData2; + private final FileData2FileDetails fileData2; /* * The size property. @@ -38,7 +38,7 @@ public final class UploadHttpPartRequest { * @param size the size value to set. */ @Generated - public UploadHttpPartRequest(InheritFileData fileData1, FileDetails fileData2, Size size) { + public UploadHttpPartRequest(InheritFileData fileData1, FileData2FileDetails fileData2, Size size) { this.fileData1 = fileData1; this.fileData2 = fileData2; this.size = size; @@ -60,7 +60,7 @@ public InheritFileData getFileData1() { * @return the fileData2 value. */ @Generated - public FileDetails getFileData2() { + public FileData2FileDetails getFileData2() { return this.fileData2; } diff --git a/packages/http-client-java/generator/http-client-generator-test/src/main/resources/META-INF/tsptest-multipart_apiview_properties.json b/packages/http-client-java/generator/http-client-generator-test/src/main/resources/META-INF/tsptest-multipart_apiview_properties.json index 80c378ecf48..2ece5c8c819 100644 --- a/packages/http-client-java/generator/http-client-generator-test/src/main/resources/META-INF/tsptest-multipart_apiview_properties.json +++ b/packages/http-client-java/generator/http-client-generator-test/src/main/resources/META-INF/tsptest-multipart_apiview_properties.json @@ -12,8 +12,8 @@ "tsptest.multipart.MultipartClient.uploadHttpPartWithResponse": "TspTest.Multipart.uploadHttpPart", "tsptest.multipart.MultipartClient.uploadWithResponse": "TspTest.Multipart.upload", "tsptest.multipart.MultipartClientBuilder": "TspTest.Multipart", + "tsptest.multipart.models.FileData2FileDetails": "TypeSpec.Http.File", "tsptest.multipart.models.FileDataFileDetails": null, - "tsptest.multipart.models.FileDetails": "TypeSpec.Http.File", "tsptest.multipart.models.FormData": "TspTest.Multipart.FormData", "tsptest.multipart.models.ImageFileDetails": null, "tsptest.multipart.models.ImageType": "TspTest.Multipart.ImageType", diff --git a/packages/http-client-java/generator/http-client-generator-test/src/main/resources/META-INF/tsptest-multipart_metadata.json b/packages/http-client-java/generator/http-client-generator-test/src/main/resources/META-INF/tsptest-multipart_metadata.json index ebc522ead6f..52b98f98465 100644 --- a/packages/http-client-java/generator/http-client-generator-test/src/main/resources/META-INF/tsptest-multipart_metadata.json +++ b/packages/http-client-java/generator/http-client-generator-test/src/main/resources/META-INF/tsptest-multipart_metadata.json @@ -1 +1 @@ -{"flavor":"Azure","crossLanguageDefinitions":{"tsptest.multipart.MultipartAsyncClient":"TspTest.Multipart","tsptest.multipart.MultipartAsyncClient.upload":"TspTest.Multipart.upload","tsptest.multipart.MultipartAsyncClient.uploadHttpPart":"TspTest.Multipart.uploadHttpPart","tsptest.multipart.MultipartAsyncClient.uploadHttpPartWithResponse":"TspTest.Multipart.uploadHttpPart","tsptest.multipart.MultipartAsyncClient.uploadWithResponse":"TspTest.Multipart.upload","tsptest.multipart.MultipartClient":"TspTest.Multipart","tsptest.multipart.MultipartClient.upload":"TspTest.Multipart.upload","tsptest.multipart.MultipartClient.uploadHttpPart":"TspTest.Multipart.uploadHttpPart","tsptest.multipart.MultipartClient.uploadHttpPartWithResponse":"TspTest.Multipart.uploadHttpPart","tsptest.multipart.MultipartClient.uploadWithResponse":"TspTest.Multipart.upload","tsptest.multipart.MultipartClientBuilder":"TspTest.Multipart","tsptest.multipart.models.FileDataFileDetails":null,"tsptest.multipart.models.FileDetails":"TypeSpec.Http.File","tsptest.multipart.models.FormData":"TspTest.Multipart.FormData","tsptest.multipart.models.ImageFileDetails":null,"tsptest.multipart.models.ImageType":"TspTest.Multipart.ImageType","tsptest.multipart.models.InheritFileData":"TspTest.Multipart.Inherit2File","tsptest.multipart.models.Size":"TspTest.Multipart.Size","tsptest.multipart.models.UploadHttpPartRequest":"TspTest.Multipart.uploadHttpPart.Request.anonymous"},"generatedFiles":["src/main/java/module-info.java","src/main/java/tsptest/multipart/MultipartAsyncClient.java","src/main/java/tsptest/multipart/MultipartClient.java","src/main/java/tsptest/multipart/MultipartClientBuilder.java","src/main/java/tsptest/multipart/implementation/MultipartClientImpl.java","src/main/java/tsptest/multipart/implementation/MultipartFormDataHelper.java","src/main/java/tsptest/multipart/implementation/package-info.java","src/main/java/tsptest/multipart/models/FileDataFileDetails.java","src/main/java/tsptest/multipart/models/FileDetails.java","src/main/java/tsptest/multipart/models/FormData.java","src/main/java/tsptest/multipart/models/ImageFileDetails.java","src/main/java/tsptest/multipart/models/ImageType.java","src/main/java/tsptest/multipart/models/InheritFileData.java","src/main/java/tsptest/multipart/models/Size.java","src/main/java/tsptest/multipart/models/UploadHttpPartRequest.java","src/main/java/tsptest/multipart/models/package-info.java","src/main/java/tsptest/multipart/package-info.java"]} \ No newline at end of file +{"flavor":"Azure","crossLanguageDefinitions":{"tsptest.multipart.MultipartAsyncClient":"TspTest.Multipart","tsptest.multipart.MultipartAsyncClient.upload":"TspTest.Multipart.upload","tsptest.multipart.MultipartAsyncClient.uploadHttpPart":"TspTest.Multipart.uploadHttpPart","tsptest.multipart.MultipartAsyncClient.uploadHttpPartWithResponse":"TspTest.Multipart.uploadHttpPart","tsptest.multipart.MultipartAsyncClient.uploadWithResponse":"TspTest.Multipart.upload","tsptest.multipart.MultipartClient":"TspTest.Multipart","tsptest.multipart.MultipartClient.upload":"TspTest.Multipart.upload","tsptest.multipart.MultipartClient.uploadHttpPart":"TspTest.Multipart.uploadHttpPart","tsptest.multipart.MultipartClient.uploadHttpPartWithResponse":"TspTest.Multipart.uploadHttpPart","tsptest.multipart.MultipartClient.uploadWithResponse":"TspTest.Multipart.upload","tsptest.multipart.MultipartClientBuilder":"TspTest.Multipart","tsptest.multipart.models.FileData2FileDetails":"TypeSpec.Http.File","tsptest.multipart.models.FileDataFileDetails":null,"tsptest.multipart.models.FormData":"TspTest.Multipart.FormData","tsptest.multipart.models.ImageFileDetails":null,"tsptest.multipart.models.ImageType":"TspTest.Multipart.ImageType","tsptest.multipart.models.InheritFileData":"TspTest.Multipart.Inherit2File","tsptest.multipart.models.Size":"TspTest.Multipart.Size","tsptest.multipart.models.UploadHttpPartRequest":"TspTest.Multipart.uploadHttpPart.Request.anonymous"},"generatedFiles":["src/main/java/module-info.java","src/main/java/tsptest/multipart/MultipartAsyncClient.java","src/main/java/tsptest/multipart/MultipartClient.java","src/main/java/tsptest/multipart/MultipartClientBuilder.java","src/main/java/tsptest/multipart/implementation/MultipartClientImpl.java","src/main/java/tsptest/multipart/implementation/MultipartFormDataHelper.java","src/main/java/tsptest/multipart/implementation/package-info.java","src/main/java/tsptest/multipart/models/FileData2FileDetails.java","src/main/java/tsptest/multipart/models/FileDataFileDetails.java","src/main/java/tsptest/multipart/models/FormData.java","src/main/java/tsptest/multipart/models/ImageFileDetails.java","src/main/java/tsptest/multipart/models/ImageType.java","src/main/java/tsptest/multipart/models/InheritFileData.java","src/main/java/tsptest/multipart/models/Size.java","src/main/java/tsptest/multipart/models/UploadHttpPartRequest.java","src/main/java/tsptest/multipart/models/package-info.java","src/main/java/tsptest/multipart/package-info.java"]} \ No newline at end of file diff --git a/packages/http-client-java/generator/http-client-generator-test/src/test/java/azure/clientgenerator/core/clientinitialization/ClientInitializationTests.java b/packages/http-client-java/generator/http-client-generator-test/src/test/java/azure/clientgenerator/core/clientinitialization/ClientInitializationTests.java deleted file mode 100644 index 7e8a8ea25af..00000000000 --- a/packages/http-client-java/generator/http-client-generator-test/src/test/java/azure/clientgenerator/core/clientinitialization/ClientInitializationTests.java +++ /dev/null @@ -1,73 +0,0 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. - -package azure.clientgenerator.core.clientinitialization; - -import azure.clientgenerator.core.clientinitialization.models.Input; -import azure.clientgenerator.core.clientinitialization.models.WithBodyRequest; -import azure.clientgenerator.core.clientinitialization.parentclient.ChildClient; -import azure.clientgenerator.core.clientinitialization.parentclient.ChildClientBuilder; -import azure.clientgenerator.core.clientinitialization.parentclient.ParentClient; -import azure.clientgenerator.core.clientinitialization.parentclient.ParentClientBuilder; -import org.junit.jupiter.api.Test; - -public final class ClientInitializationTests { - - private static final String TEST_NAME = "test-name-value"; - private static final String TEST_ID = "test-id"; - private static final Input TEST_BODY = new Input("test-name"); - private static final String TEST_REGION = "us-west"; - private static final String TEST_BLOB = "sample-blob"; - - @Test - public void testHeader() { - HeaderParamClient client = new HeaderParamClientBuilder().name(TEST_NAME).buildClient(); - client.withQuery(TEST_ID); - client.withBody(TEST_BODY); - } - - @Test - public void testPath() { - PathParamClient client = new PathParamClientBuilder().blobName(TEST_BLOB).buildClient(); - client.withQuery("text"); - client.getStandalone(); - client.deleteStandalone(); - } - - @Test - public void testMixed() { - MixedParamsClient client = new MixedParamsClientBuilder().name(TEST_NAME).buildClient(); - client.withQuery(TEST_REGION, TEST_ID); - client.withBody(TEST_REGION, new WithBodyRequest("test-name")); - } - - @Test - public void testMultiple() { - MultipleParamsClient client - = new MultipleParamsClientBuilder().name(TEST_NAME).region(TEST_REGION).buildClient(); - client.withQuery(TEST_ID); - client.withBody(TEST_BODY); - } - - @Test - public void testParamAlias() { - ParamAliasClient client = new ParamAliasClientBuilder().blobName(TEST_BLOB).buildClient(); - client.withOriginalName(); - client.withAliasedName(); - } - - @Test - public void testChildClient() { - // ChildClient via ParentClient - ParentClient parentClient = new ParentClientBuilder().buildClient(); - ChildClient childClient = parentClient.getChildClient(TEST_BLOB); - - childClient.getStandalone(); - childClient.deleteStandalone(); - - // ChildClient via ChildClientBuilder - childClient = new ChildClientBuilder().blobName(TEST_BLOB).buildClient(); - - childClient.withQuery("text"); - } -}