From 4f48009230d29e7d17c6530f797590ec32a36080 Mon Sep 17 00:00:00 2001 From: Weidong Xu Date: Fri, 26 Jan 2024 10:30:42 +0800 Subject: [PATCH] tsp, multipart with ##FileDetails class (#2525) --- .../azure/autorest/mapper/ClientMapper.java | 9 +- .../azure/autorest/mapper/ModelMapper.java | 45 +++------ .../template/ClientMethodTemplateBase.java | 3 +- .../ConvenienceMethodTemplateBase.java | 86 +++++++++------- .../autorest/template/ModelTemplate.java | 3 +- .../azure/autorest/util/ClientModelUtil.java | 87 ++++++++++++++++- .../resources/MultipartFormDataHelper.java | 42 +++++--- typespec-extension/changelog.md | 6 ++ typespec-extension/package-lock.json | 4 +- typespec-extension/package.json | 2 +- typespec-extension/src/code-model-builder.ts | 19 ++-- .../com/azure/autorest/TypeSpecPlugin.java | 3 +- typespec-tests/package.json | 2 +- .../cadl/multipart/MultipartAsyncClient.java | 24 ++++- .../com/cadl/multipart/MultipartClient.java | 24 ++++- .../MultipartFormDataHelper.java | 36 ++++--- .../cadl/multipart/models/FileDetails.java | 97 +++++++++++++++++++ .../com/cadl/multipart/models/FormData.java | 28 +++--- .../multipart/models/ImageFileDetails.java | 97 +++++++++++++++++++ .../multipart/MultiPartAsyncClient.java | 48 ++++++--- .../payload/multipart/MultiPartClient.java | 45 ++++++--- .../MultipartFormDataHelper.java | 36 ++++--- .../models/BinaryArrayPartsRequest.java | 39 +------- .../multipart/models/ComplexPartsRequest.java | 73 ++------------ .../models/JsonArrayPartsRequest.java | 39 +------- .../multipart/models/JsonPartRequest.java | 39 +------- .../models/MultiBinaryPartsRequest.java | 69 ++----------- .../multipart/models/MultiPartRequest.java | 39 +------- .../multipart/models/PictureFileDetails.java | 97 +++++++++++++++++++ .../multipart/models/PicturesFileDetails.java | 97 +++++++++++++++++++ .../models/ProfileImageFileDetails.java | 97 +++++++++++++++++++ .../com/payload/multipart/MultipartTests.java | 77 +++++++++++---- typespec-tests/tsp/multipart.tsp | 1 + 33 files changed, 952 insertions(+), 461 deletions(-) create mode 100644 typespec-tests/src/main/java/com/cadl/multipart/models/FileDetails.java create mode 100644 typespec-tests/src/main/java/com/cadl/multipart/models/ImageFileDetails.java create mode 100644 typespec-tests/src/main/java/com/payload/multipart/models/PictureFileDetails.java create mode 100644 typespec-tests/src/main/java/com/payload/multipart/models/PicturesFileDetails.java create mode 100644 typespec-tests/src/main/java/com/payload/multipart/models/ProfileImageFileDetails.java diff --git a/javagen/src/main/java/com/azure/autorest/mapper/ClientMapper.java b/javagen/src/main/java/com/azure/autorest/mapper/ClientMapper.java index 76742ed8d1..81b012e21f 100644 --- a/javagen/src/main/java/com/azure/autorest/mapper/ClientMapper.java +++ b/javagen/src/main/java/com/azure/autorest/mapper/ClientMapper.java @@ -33,6 +33,7 @@ import com.azure.autorest.model.clientmodel.ClientMethodExample; import com.azure.autorest.model.clientmodel.ClientMethodType; import com.azure.autorest.model.clientmodel.ClientModel; +import com.azure.autorest.model.clientmodel.ClientModels; import com.azure.autorest.model.clientmodel.ClientResponse; import com.azure.autorest.model.clientmodel.ConvenienceMethod; import com.azure.autorest.model.clientmodel.EnumType; @@ -132,13 +133,17 @@ public Client map(CodeModel codeModel) { codeModel.getOperationGroups().stream().flatMap(og -> og.getOperations().stream()) .map(o -> parseHeader(o, settings)).filter(Objects::nonNull)); - final List clientModels = autoRestModelTypes + List clientModelsFromCodeModel = autoRestModelTypes .distinct() .map(autoRestCompositeType -> Mappers.getModelMapper().map(autoRestCompositeType)) .filter(Objects::nonNull) .distinct() .collect(Collectors.toList()); - + // append some models not from CodeModel (currently, only for ##FileDetails models for multipart/form-data request) + // TODO (weidxu): we can remove this code block, if ##FileDetails moves to azure-core + final List clientModels = Stream.concat(clientModelsFromCodeModel.stream(), ClientModels.getInstance().getModels().stream()) + .distinct() + .collect(Collectors.toList()); builder.models(clientModels); // union model (class) diff --git a/javagen/src/main/java/com/azure/autorest/mapper/ModelMapper.java b/javagen/src/main/java/com/azure/autorest/mapper/ModelMapper.java index f20fa0961e..a2244f555b 100644 --- a/javagen/src/main/java/com/azure/autorest/mapper/ModelMapper.java +++ b/javagen/src/main/java/com/azure/autorest/mapper/ModelMapper.java @@ -11,6 +11,7 @@ import com.azure.autorest.extension.base.model.codemodel.ObjectSchema; import com.azure.autorest.extension.base.model.codemodel.Property; import com.azure.autorest.extension.base.model.codemodel.Schema; +import com.azure.autorest.extension.base.model.codemodel.SchemaContext; import com.azure.autorest.extension.base.model.codemodel.XmlSerlializationFormat; import com.azure.autorest.extension.base.plugin.JavaSettings; import com.azure.autorest.model.clientmodel.ArrayType; @@ -307,7 +308,7 @@ public ClientModel map(ObjectSchema compositeType) { // handle multipart/form-data if (!CoreUtils.isNullOrEmpty(compositeType.getSerializationFormats()) && compositeType.getSerializationFormats().contains(KnownMediaType.MULTIPART.value())) { - processMultipartFormDataProperties(properties); + processMultipartFormDataProperties(compositeType, properties); } builder.properties(properties); @@ -586,45 +587,31 @@ private static String disambiguatePropertyNameOfFlattenedSchema(Set prop return ret; } - private static void processMultipartFormDataProperties(List properties) { + private static void processMultipartFormDataProperties(ObjectSchema compositeType, List properties) { + if (compositeType.getUsage() == null || !(compositeType.getUsage().contains(SchemaContext.PUBLIC) || compositeType.getUsage().contains(SchemaContext.INTERNAL))) { + // not need to process, if this model does not write to a class + return; + } + ListIterator iterator = properties.listIterator(); while (iterator.hasNext()) { ClientModelProperty property = iterator.next(); if (property.getWireType() == ArrayType.BYTE_ARRAY) { - // replace byte[] with BinaryData + IType fileDetailsModelType = ClientModelUtil.getMultipartFileDetailsModel(compositeType, property.getName()); + // replace byte[] with the type iterator.remove(); iterator.add(property.newBuilder() - .wireType(ClassType.BINARY_DATA) - .clientType(ClassType.BINARY_DATA) - .build()); - - // add (optional) filename property - // here is a hack to use same serializedName - iterator.add(property.newBuilder() - .name(property.getName() + ClientModelUtil.FILENAME_SUFFIX) - .defaultValue(ClassType.STRING.defaultValueExpression(property.getSerializedName())) - .description("The filename for " + property.getName()) - .wireType(ClassType.STRING) - .clientType(ClassType.STRING) - .required(false) + .wireType(fileDetailsModelType) + .clientType(fileDetailsModelType) .build()); } else if (property.getWireType() instanceof ListType && ((ListType) property.getWireType()).getElementType() == ArrayType.BYTE_ARRAY) { - // replace List with List + IType fileDetailsModelType = ClientModelUtil.getMultipartFileDetailsModel(compositeType, property.getName()); + // replace List with List iterator.remove(); iterator.add(property.newBuilder() - .wireType(new ListType(ClassType.BINARY_DATA)) - .clientType(new ListType(ClassType.BINARY_DATA)) - .build()); - - // add (optional) filenames property as List - // here is a hack to use same serializedName - iterator.add(property.newBuilder() - .name(property.getName() + ClientModelUtil.FILENAME_SUFFIX + "s") - .description("The filenames for " + property.getName()) - .wireType(new ListType(ClassType.STRING)) - .clientType(new ListType(ClassType.STRING)) - .required(false) + .wireType(new ListType(fileDetailsModelType)) + .clientType(new ListType(fileDetailsModelType)) .build()); } } diff --git a/javagen/src/main/java/com/azure/autorest/template/ClientMethodTemplateBase.java b/javagen/src/main/java/com/azure/autorest/template/ClientMethodTemplateBase.java index 411bc8b722..742ffb0ef6 100644 --- a/javagen/src/main/java/com/azure/autorest/template/ClientMethodTemplateBase.java +++ b/javagen/src/main/java/com/azure/autorest/template/ClientMethodTemplateBase.java @@ -3,7 +3,6 @@ package com.azure.autorest.template; -import com.azure.autorest.extension.base.model.codemodel.KnownMediaType; import com.azure.autorest.extension.base.model.codemodel.RequestParameterLocation; import com.azure.autorest.extension.base.plugin.JavaSettings; import com.azure.autorest.model.clientmodel.ClassType; @@ -72,7 +71,7 @@ protected static void generateProtocolMethodJavadoc(ClientMethod clientMethod, J if (bodyParameter.isPresent()) { ClientModel model = ClientModelUtil.getClientModel(bodyParameter.get().getRawType().toString()); - if (model == null || !model.getSerializationFormats().contains(KnownMediaType.MULTIPART.value())) { + if (model == null || !ClientModelUtil.isMultipartModel(model)) { // do not generate JSON schema for Multipart request body boolean isBodyParamRequired = bodyParameter.map(ProxyMethodParameter::isRequired).orElse(false); bodyParameter.map(ProxyMethodParameter::getRawType).ifPresent(type -> requestBodySchemaJavadoc(type, commentBlock, typesInJavadoc, isBodyParamRequired)); diff --git a/javagen/src/main/java/com/azure/autorest/template/ConvenienceMethodTemplateBase.java b/javagen/src/main/java/com/azure/autorest/template/ConvenienceMethodTemplateBase.java index b402e7274e..4f2a19efe6 100644 --- a/javagen/src/main/java/com/azure/autorest/template/ConvenienceMethodTemplateBase.java +++ b/javagen/src/main/java/com/azure/autorest/template/ConvenienceMethodTemplateBase.java @@ -3,7 +3,6 @@ package com.azure.autorest.template; -import com.azure.autorest.extension.base.model.codemodel.KnownMediaType; import com.azure.autorest.extension.base.model.codemodel.RequestParameterLocation; import com.azure.autorest.extension.base.plugin.JavaSettings; import com.azure.autorest.model.clientmodel.Annotation; @@ -43,15 +42,14 @@ import java.util.Collection; import java.util.Collections; import java.util.HashMap; -import java.util.HashSet; import java.util.LinkedHashMap; import java.util.List; import java.util.Locale; import java.util.Map; import java.util.Objects; -import java.util.Optional; import java.util.Set; import java.util.TreeMap; +import java.util.function.BiFunction; import java.util.function.Consumer; import java.util.function.Function; import java.util.stream.Collectors; @@ -596,7 +594,7 @@ private static String expressionConvertToType(String name, MethodParameter conve if (bodyType instanceof ClassType) { ClientModel model = ClientModelUtil.getClientModel(bodyType.toString()); // serialize model for multipart/form-data - if (model != null && model.getSerializationFormats().contains(KnownMediaType.MULTIPART.value())) { + if (model != null && ClientModelUtil.isMultipartModel(model)) { return expressionMultipartFormDataToBinaryData(name, model); } } @@ -624,63 +622,69 @@ private static String expressionConvertToType(String name, MethodParameter conve } private static String expressionMultipartFormDataToBinaryData(String name, ClientModel model) { - // find corresponding filename property - Function> findFileNameProperty = - (serializedName) -> model.getProperties().stream() - // here is a hack to find matching filename property by finding property of type String/List and of same serializedName - .filter(p -> Objects.equals(serializedName, p.getSerializedName()) - && (p.getWireType() == ClassType.STRING || (p.getWireType() instanceof ListType && (((ListType) p.getWireType()).getElementType() == ClassType.STRING)))) - .findFirst(); + BiFunction nullableExpression = (propertyExpr, expr) -> propertyExpr + " == null ? null : " + expr; // serialize model for multipart/form-data StringBuilder builder = new StringBuilder().append("new MultipartFormDataHelper(requestOptions)"); - Set filePropertySerializedNames = new HashSet<>(); for (ClientModelProperty property : model.getProperties()) { - if (property.getWireType() == ClassType.BINARY_DATA) { - // application/octet-stream - String serializedName = property.getSerializedName(); - filePropertySerializedNames.add(serializedName); - - String filenameExpression = findFileNameProperty.apply(serializedName) - .map(clientModelProperty -> name + "." + clientModelProperty.getGetterName() + "()") - .orElse(ClassType.STRING.defaultValueExpression(property.getSerializedName())); + String propertyGetExpression = name + "." + property.getGetterName() + "()"; + if (isMultipartModel(property.getWireType())) { + // file, usually application/octet-stream + + String fileExpression = propertyGetExpression + ".getContent()"; + String contentTypeExpression = propertyGetExpression + ".getContentType()"; + String filenameExpression = propertyGetExpression + ".getFilename()"; + if (!property.isRequired()) { + fileExpression = nullableExpression.apply(propertyGetExpression, fileExpression); + contentTypeExpression = nullableExpression.apply(propertyGetExpression, contentTypeExpression); + filenameExpression = nullableExpression.apply(propertyGetExpression, filenameExpression); + } builder.append(String.format( - ".serializeFileField(%1$s, %2$s.%3$s(), %4$s)", + ".serializeFileField(%1$s, %2$s, %3$s, %4$s)", ClassType.STRING.defaultValueExpression(property.getSerializedName()), - name, property.getGetterName(), + fileExpression, + contentTypeExpression, filenameExpression )); - } else if (property.getWireType() instanceof ListType && ((ListType) property.getWireType()).getElementType() == ClassType.BINARY_DATA) { - // application/octet-stream, multiple files - String serializedName = property.getSerializedName(); - filePropertySerializedNames.add(serializedName); - - String filenameExpression = findFileNameProperty.apply(serializedName) - .map(clientModelProperty -> name + "." + clientModelProperty.getGetterName() + "()") - .orElse("null"); + } else if (property.getWireType() instanceof ListType && isMultipartModel(((ListType) property.getWireType()).getElementType())) { + // file array + + // For now, we use 3 List, as we do not wish the Helper class refer to different ##FileDetails model. + // Later, if we switch to a shared class in azure-core, we can change the implementation. + String className = ((ListType) property.getWireType()).getElementType().toString(); + String streamExpressionFormat = "%1$s.stream().map(%2$s::%3$s).collect(Collectors.toList())"; + String fileExpression = String.format(streamExpressionFormat, + propertyGetExpression, className, "getContent"); + String contentTypeExpression = String.format(streamExpressionFormat, + propertyGetExpression, className, "getContentType"); + String filenameExpression = String.format(streamExpressionFormat, + propertyGetExpression, className, "getFilename"); + if (!property.isRequired()) { + fileExpression = nullableExpression.apply(propertyGetExpression, fileExpression); + contentTypeExpression = nullableExpression.apply(propertyGetExpression, contentTypeExpression); + filenameExpression = nullableExpression.apply(propertyGetExpression, filenameExpression); + } builder.append(String.format( - ".serializeFileFields(%1$s, %2$s.%3$s(), %4$s)", + ".serializeFileFields(%1$s, %2$s, %3$s, %4$s)", ClassType.STRING.defaultValueExpression(property.getSerializedName()), - name, property.getGetterName(), + fileExpression, + contentTypeExpression, filenameExpression )); - } else if (filePropertySerializedNames.contains(property.getSerializedName())) { - // skip filename property } else if (ClientModelUtil.isClientModel(property.getWireType()) || property.getWireType() instanceof MapType || property.getWireType() instanceof IterableType) { // application/json - String stringExpression = name + "." + property.getGetterName() + "()"; builder.append(String.format( ".serializeJsonField(%1$s, %2$s)", ClassType.STRING.defaultValueExpression(property.getSerializedName()), - stringExpression + propertyGetExpression )); } else { // text/plain - String stringExpression = name + "." + property.getGetterName() + "()"; + String stringExpression = propertyGetExpression; // convert to String if (property.getWireType() instanceof PrimitiveType) { stringExpression = String.format("String.valueOf(%s)", stringExpression); @@ -698,6 +702,14 @@ private static String expressionMultipartFormDataToBinaryData(String name, Clien return builder.toString(); } + private static boolean isMultipartModel(IType type) { + if (ClientModelUtil.isClientModel(type)) { + return ClientModelUtil.isMultipartModel(ClientModelUtil.getClientModel(type.toString())); + } else { + return false; + } + } + private static Map findParametersForConvenienceMethod( ClientMethod convenienceMethod, ClientMethod protocolMethod) { Map parameterMap = new LinkedHashMap<>(); diff --git a/javagen/src/main/java/com/azure/autorest/template/ModelTemplate.java b/javagen/src/main/java/com/azure/autorest/template/ModelTemplate.java index 2bd4981eda..4735718d61 100644 --- a/javagen/src/main/java/com/azure/autorest/template/ModelTemplate.java +++ b/javagen/src/main/java/com/azure/autorest/template/ModelTemplate.java @@ -3,7 +3,6 @@ package com.azure.autorest.template; -import com.azure.autorest.extension.base.model.codemodel.KnownMediaType; import com.azure.autorest.extension.base.plugin.JavaSettings; import com.azure.autorest.model.clientmodel.Annotation; import com.azure.autorest.model.clientmodel.ArrayType; @@ -1144,7 +1143,7 @@ private static boolean isGenerateConstantEmptyByteArray(ClientModel model, JavaS */ private static boolean modelRequireSerialization(ClientModel model) { // TODO (weidxu): any other case? "binary"? - return !model.getSerializationFormats().contains(KnownMediaType.MULTIPART.value()); + return !ClientModelUtil.isMultipartModel(model); } /** diff --git a/javagen/src/main/java/com/azure/autorest/util/ClientModelUtil.java b/javagen/src/main/java/com/azure/autorest/util/ClientModelUtil.java index 2a64977737..8bef48130b 100644 --- a/javagen/src/main/java/com/azure/autorest/util/ClientModelUtil.java +++ b/javagen/src/main/java/com/azure/autorest/util/ClientModelUtil.java @@ -7,6 +7,10 @@ import com.azure.autorest.extension.base.model.codemodel.Client; import com.azure.autorest.extension.base.model.codemodel.CodeModel; import com.azure.autorest.extension.base.model.codemodel.ConstantSchema; +import com.azure.autorest.extension.base.model.codemodel.KnownMediaType; +import com.azure.autorest.extension.base.model.codemodel.Language; +import com.azure.autorest.extension.base.model.codemodel.Languages; +import com.azure.autorest.extension.base.model.codemodel.ObjectSchema; import com.azure.autorest.extension.base.model.codemodel.OperationGroup; import com.azure.autorest.extension.base.model.codemodel.Parameter; import com.azure.autorest.extension.base.plugin.JavaSettings; @@ -51,10 +55,6 @@ */ public class ClientModelUtil { - // used for filename property of multipart/form-data - // e.g. if property for file is "BinaryData audio", a "String audioFilename" will be added to the ClientModel. - public static final String FILENAME_SUFFIX = "Filename"; - public static final String MULTI_PART_FORM_DATA_HELPER_CLASS_NAME = "MultipartFormDataHelper"; public static final String CORE_TO_CODEGEN_BRIDGE_UTILS_CLASS_NAME = "CoreToCodegenBridgeUtils"; @@ -714,4 +714,83 @@ public static Set getExternalPackageNamesUsedInClient(List return externalPackageNames; } + + /** + * Gets or creates a new FileDetails model for a multipart/form-data request + * + * @param compositeType the object schema of the multipart/form-data request model. + * @param filePropertyName the property name of the file in the multipart/form-data request model. + * @return the ##FileDetails model + */ + public static IType getMultipartFileDetailsModel( + ObjectSchema compositeType, + String filePropertyName) { + // TODO (weidxu): this ##FileDetails model may get renamed and moved to azure-core + + // The ##FileDetails model would inherit the usages from compositeType (the request model). So if the request is INTERNAL, FileDetails model would also be INTERNAL. + // But it may reside in a different package, depending on the options e.g. "custom-types"/"custom-types-subpackage". + + String fileDetailsModelName = com.azure.autorest.preprocessor.namer.CodeNamer.getTypeName( + filePropertyName.toLowerCase(Locale.ROOT).endsWith("file") + ? filePropertyName + "Details" + : filePropertyName + "FileDetails"); + ClientModel clientModel = ClientModelUtil.getClientModel(fileDetailsModelName); + if (clientModel != null) { + return clientModel.getType(); + } + + // create ClassType + ObjectSchema objectSchema = new ObjectSchema(); + objectSchema.setLanguage(new Languages()); + objectSchema.getLanguage().setJava(new Language()); + objectSchema.getLanguage().getJava().setName(fileDetailsModelName); + objectSchema.setUsage(compositeType.getUsage()); + ClassType type = Mappers.getObjectMapper().map(objectSchema); + + // create ClientModel + List properties = new ArrayList<>(); + properties.add(new ClientModelProperty.Builder() + .name("content") + .description("The content of the file") + .required(true) + .readOnly(false) + .wireType(ClassType.BINARY_DATA) + .clientType(ClassType.BINARY_DATA) + .build()); + properties.add(new ClientModelProperty.Builder() + .name("filename") + .description("The filename of the file") + .required(false) + .readOnly(false) + .wireType(ClassType.STRING) + .clientType(ClassType.STRING) + .build()); + properties.add(new ClientModelProperty.Builder() + .name("contentType") + .description("The content-type of the file") + .required(false) + .readOnly(false) + .wireType(ClassType.STRING) + .clientType(ClassType.STRING) + .defaultValue("\"application/octet-stream\"") + .build()); + clientModel = new ClientModel.Builder() + .name(fileDetailsModelName) + .description("The file details model for the " + filePropertyName) + .packageName(type.getPackage()) + .type(type) + .serializationFormats(Set.of(KnownMediaType.MULTIPART.value())) + // let it inherit the usage (PUBLIC/INTERNAL) from the multipart/form-data request model + .implementationDetails(new ImplementationDetails.Builder() + .usages(SchemaUtil.mapSchemaContext(compositeType.getUsage())) + .build()) + .properties(properties) + .build(); + ClientModels.getInstance().addModel(clientModel); + return clientModel.getType(); + } + + public static boolean isMultipartModel(ClientModel model) { + return model.getSerializationFormats().contains(KnownMediaType.MULTIPART.value()); + } } diff --git a/javagen/src/main/resources/MultipartFormDataHelper.java b/javagen/src/main/resources/MultipartFormDataHelper.java index c04ef87a96..f4c2ab5601 100644 --- a/javagen/src/main/resources/MultipartFormDataHelper.java +++ b/javagen/src/main/resources/MultipartFormDataHelper.java @@ -20,6 +20,8 @@ public final class MultipartFormDataHelper { */ private static final String CRLF = "\r\n"; + private static final String APPLICATION_OCTET_STREAM = "application/octet-stream"; + /** * Value to be used as part of the divider for the multipart requests. */ @@ -111,48 +113,62 @@ public MultipartFormDataHelper serializeJsonField(String fieldName, Object jsonO return this; } - // application/octet-stream /** - * Formats a application/octet-stream field for a multipart HTTP request. + * Formats a file field for a multipart HTTP request. * * @param fieldName the field name * @param file the BinaryData of the file + * @param contentType the content-type of the file * @param filename the filename * @return the MultipartFormDataHelper instance */ - public MultipartFormDataHelper serializeFileField(String fieldName, BinaryData file, String filename) { + public MultipartFormDataHelper serializeFileField( + String fieldName, + BinaryData file, + String contentType, + String filename) { if (file != null) { + if (CoreUtils.isNullOrEmpty(contentType)) { + contentType = APPLICATION_OCTET_STREAM; + } if (CoreUtils.isNullOrEmpty(filename)) { filename = fieldName; } filename = normalizeAscii(filename); - writeFileField(fieldName, file, filename); + writeFileField(fieldName, file, contentType, filename); } return this; } - // application/octet-stream, multiple files /** - * Formats a application/octet-stream field (potentially multiple files) for a multipart HTTP request. + * Formats a file field (potentially multiple files) for a multipart HTTP request. * * @param fieldName the field name * @param files the List of BinaryData of the files - * @param filenames the List of filenames. - * If it is {@code null}, or the size of the List is smaller than that of "files", implementation-specific filename is used. + * @param contentTypes the List of content-type of the files + * @param filenames the List of filenames * @return the MultipartFormDataHelper instance */ - public MultipartFormDataHelper serializeFileFields(String fieldName, List files, List filenames) { + public MultipartFormDataHelper serializeFileFields( + String fieldName, + List files, + List contentTypes, + List filenames) { if (files != null) { for (int i = 0; i < files.size(); ++i) { BinaryData file = files.get(i); - String filename = (filenames != null && filenames.size() > i) ? filenames.get(i) : null; + String contentType = contentTypes.get(i); + if (CoreUtils.isNullOrEmpty(contentType)) { + contentType = APPLICATION_OCTET_STREAM; + } + String filename = filenames.get(i); if (CoreUtils.isNullOrEmpty(filename)) { filename = fieldName + String.valueOf(i + 1); } filename = normalizeAscii(filename); - writeFileField(fieldName, file, filename); + writeFileField(fieldName, file, contentType, filename); } } return this; @@ -176,12 +192,12 @@ public MultipartFormDataHelper end() { return this; } - private void writeFileField(String fieldName, BinaryData file, String filename) { + private void writeFileField(String fieldName, BinaryData file, String contentType, String filename) { // Multipart preamble String fileFieldPreamble = partSeparator + CRLF + "Content-Disposition: form-data; name=\"" + fieldName + "\"; filename=\"" + filename + "\"" - + CRLF + "Content-Type: application/octet-stream" + CRLF + CRLF; + + CRLF + "Content-Type: " + contentType + CRLF + CRLF; byte[] data = fileFieldPreamble.getBytes(encoderCharset); appendBytes(data); diff --git a/typespec-extension/changelog.md b/typespec-extension/changelog.md index 96c6aab678..d84eaea1df 100644 --- a/typespec-extension/changelog.md +++ b/typespec-extension/changelog.md @@ -1,5 +1,11 @@ # Release History +## 0.13.1 (2024-01-26) + +Compatible with compiler 0.52. + +- Behavior changed on "multipart/form-data" request. The file field would take a `##FileDetails` model, instead of `BinaryData`. + ## 0.13.0 (2024-01-25) Compatible with compiler 0.52. diff --git a/typespec-extension/package-lock.json b/typespec-extension/package-lock.json index 427ed4e487..755ab45738 100644 --- a/typespec-extension/package-lock.json +++ b/typespec-extension/package-lock.json @@ -1,12 +1,12 @@ { "name": "@azure-tools/typespec-java", - "version": "0.13.0", + "version": "0.13.1", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@azure-tools/typespec-java", - "version": "0.13.0", + "version": "0.13.1", "license": "MIT", "dependencies": { "@autorest/codemodel": "~4.20.0", diff --git a/typespec-extension/package.json b/typespec-extension/package.json index ae196c3cee..b37e2b568c 100644 --- a/typespec-extension/package.json +++ b/typespec-extension/package.json @@ -1,6 +1,6 @@ { "name": "@azure-tools/typespec-java", - "version": "0.13.0", + "version": "0.13.1", "description": "TypeSpec library for emitting Java client from the TypeSpec REST protocol binding", "keywords": [ "TypeSpec" diff --git a/typespec-extension/src/code-model-builder.ts b/typespec-extension/src/code-model-builder.ts index f53c36847c..28ec3eb472 100644 --- a/typespec-extension/src/code-model-builder.ts +++ b/typespec-extension/src/code-model-builder.ts @@ -573,18 +573,18 @@ export class CodeModelBuilder { // operation group with no operation is skipped if (operations.length > 0) { const groupPath = operationGroup.groupPath.split("."); - let oprationGroupName: string; + let operationGroupName: string; if (groupPath.length > 1) { // groupPath should be in format of "OpenAIClient.Chat.Completions" - oprationGroupName = groupPath.slice(1).join(""); + operationGroupName = groupPath.slice(1).join(""); } else { // protection - oprationGroupName = operationGroup.type.name; + operationGroupName = operationGroup.type.name; } - codeModelGroup = new OperationGroup(oprationGroupName); + codeModelGroup = new OperationGroup(operationGroupName); for (const operation of operations) { if (!this.needToSkipProcessingOperation(operation, clientContext)) { - codeModelGroup.addOperation(this.processOperation(oprationGroupName, operation, clientContext)); + codeModelGroup.addOperation(this.processOperation(operationGroupName, operation, clientContext)); } } codeModelClient.operationGroups.push(codeModelGroup); @@ -1300,7 +1300,7 @@ export class CodeModelBuilder { this.trackSchemaUsage(schema, { serializationFormats: [KnownMediaType.Multipart] }); } - if (!schema.language.default.name && schema instanceof ObjectSchema) { + if (schema instanceof ObjectSchema && !schema.language.default.name) { // anonymous model // name the schema for documentation @@ -1310,6 +1310,13 @@ export class CodeModelBuilder { // name the parameter for documentation parameter.language.default.name = "request"; } + + if (schema.serializationFormats?.includes(KnownMediaType.Multipart)) { + // TODO: anonymous model for multipart is not supported + // at present, use the model with name given above + return; + } + this.trackSchemaUsage(schema, { usage: [SchemaContext.Anonymous] }); if (op.convenienceApi && op.parameters) { diff --git a/typespec-extension/src/main/java/com/azure/autorest/TypeSpecPlugin.java b/typespec-extension/src/main/java/com/azure/autorest/TypeSpecPlugin.java index 42228ba952..f524d04751 100644 --- a/typespec-extension/src/main/java/com/azure/autorest/TypeSpecPlugin.java +++ b/typespec-extension/src/main/java/com/azure/autorest/TypeSpecPlugin.java @@ -6,7 +6,6 @@ import com.azure.autorest.extension.base.jsonrpc.Connection; import com.azure.autorest.extension.base.model.Message; import com.azure.autorest.extension.base.model.codemodel.CodeModel; -import com.azure.autorest.extension.base.model.codemodel.KnownMediaType; import com.azure.autorest.extension.base.plugin.JavaSettings; import com.azure.autorest.mapper.Mappers; import com.azure.autorest.model.clientmodel.AsyncSyncClient; @@ -159,7 +158,7 @@ protected void writeHelperClasses(Client client, JavaPackage javaPackage, JavaSe // MultipartFormDataHelper final boolean generateMultipartFormDataHelper = client.getModels().stream() .filter(ModelUtil::isGeneratingModel) - .anyMatch(m -> m.getSerializationFormats().contains(KnownMediaType.MULTIPART.value())); + .anyMatch(ClientModelUtil::isMultipartModel); if (generateMultipartFormDataHelper) { javaPackage.addJavaFromResources(settings.getPackage(settings.getImplementationSubpackage()), ClientModelUtil.MULTI_PART_FORM_DATA_HELPER_CLASS_NAME); } diff --git a/typespec-tests/package.json b/typespec-tests/package.json index 616d329185..dc2188bb4a 100644 --- a/typespec-tests/package.json +++ b/typespec-tests/package.json @@ -10,7 +10,7 @@ }, "dependencies": { "@azure-tools/cadl-ranch-specs": "0.28.7", - "@azure-tools/typespec-java": "file:/../typespec-extension/azure-tools-typespec-java-0.13.0.tgz" + "@azure-tools/typespec-java": "file:/../typespec-extension/azure-tools-typespec-java-0.13.1.tgz" }, "devDependencies": { "@typespec/prettier-plugin-typespec": "~0.52.0", diff --git a/typespec-tests/src/main/java/com/cadl/multipart/MultipartAsyncClient.java b/typespec-tests/src/main/java/com/cadl/multipart/MultipartAsyncClient.java index 8084099ffa..701d01bdb4 100644 --- a/typespec-tests/src/main/java/com/cadl/multipart/MultipartAsyncClient.java +++ b/typespec-tests/src/main/java/com/cadl/multipart/MultipartAsyncClient.java @@ -18,8 +18,10 @@ import com.azure.core.util.FluxUtil; import com.cadl.multipart.implementation.MultipartClientImpl; import com.cadl.multipart.implementation.MultipartFormDataHelper; +import com.cadl.multipart.models.FileDetails; import com.cadl.multipart.models.FormData; import java.util.Objects; +import java.util.stream.Collectors; import reactor.core.publisher.Mono; /** @@ -105,7 +107,16 @@ public Mono upload(String name, FormData data, Boolean compress) { new MultipartFormDataHelper(requestOptions).serializeTextField("name", data.getName()) .serializeTextField("resolution", String.valueOf(data.getResolution())) .serializeTextField("type", Objects.toString(data.getType())).serializeJsonField("size", data.getSize()) - .serializeFileField("image", data.getImage(), data.getImageFilename()).end().getRequestBody(), + .serializeFileField("image", data.getImage().getContent(), data.getImage().getContentType(), + data.getImage().getFilename()) + .serializeFileFields("file", + data.getFile() == null ? null + : data.getFile().stream().map(FileDetails::getContent).collect(Collectors.toList()), + data.getFile() == null ? null + : data.getFile().stream().map(FileDetails::getContentType).collect(Collectors.toList()), + data.getFile() == null ? null + : data.getFile().stream().map(FileDetails::getFilename).collect(Collectors.toList())) + .end().getRequestBody(), requestOptions).flatMap(FluxUtil::toMono); } @@ -131,7 +142,16 @@ public Mono upload(String name, FormData data) { new MultipartFormDataHelper(requestOptions).serializeTextField("name", data.getName()) .serializeTextField("resolution", String.valueOf(data.getResolution())) .serializeTextField("type", Objects.toString(data.getType())).serializeJsonField("size", data.getSize()) - .serializeFileField("image", data.getImage(), data.getImageFilename()).end().getRequestBody(), + .serializeFileField("image", data.getImage().getContent(), data.getImage().getContentType(), + data.getImage().getFilename()) + .serializeFileFields("file", + data.getFile() == null ? null + : data.getFile().stream().map(FileDetails::getContent).collect(Collectors.toList()), + data.getFile() == null ? null + : data.getFile().stream().map(FileDetails::getContentType).collect(Collectors.toList()), + data.getFile() == null ? null + : data.getFile().stream().map(FileDetails::getFilename).collect(Collectors.toList())) + .end().getRequestBody(), requestOptions).flatMap(FluxUtil::toMono); } } diff --git a/typespec-tests/src/main/java/com/cadl/multipart/MultipartClient.java b/typespec-tests/src/main/java/com/cadl/multipart/MultipartClient.java index 4bcef6e123..3407ddcbe0 100644 --- a/typespec-tests/src/main/java/com/cadl/multipart/MultipartClient.java +++ b/typespec-tests/src/main/java/com/cadl/multipart/MultipartClient.java @@ -17,8 +17,10 @@ import com.azure.core.util.BinaryData; import com.cadl.multipart.implementation.MultipartClientImpl; import com.cadl.multipart.implementation.MultipartFormDataHelper; +import com.cadl.multipart.models.FileDetails; import com.cadl.multipart.models.FormData; import java.util.Objects; +import java.util.stream.Collectors; /** * Initializes a new instance of the synchronous MultipartClient type. @@ -102,7 +104,16 @@ public void upload(String name, FormData data, Boolean compress) { new MultipartFormDataHelper(requestOptions).serializeTextField("name", data.getName()) .serializeTextField("resolution", String.valueOf(data.getResolution())) .serializeTextField("type", Objects.toString(data.getType())).serializeJsonField("size", data.getSize()) - .serializeFileField("image", data.getImage(), data.getImageFilename()).end().getRequestBody(), + .serializeFileField("image", data.getImage().getContent(), data.getImage().getContentType(), + data.getImage().getFilename()) + .serializeFileFields("file", + data.getFile() == null ? null + : data.getFile().stream().map(FileDetails::getContent).collect(Collectors.toList()), + data.getFile() == null ? null + : data.getFile().stream().map(FileDetails::getContentType).collect(Collectors.toList()), + data.getFile() == null ? null + : data.getFile().stream().map(FileDetails::getFilename).collect(Collectors.toList())) + .end().getRequestBody(), requestOptions).getValue(); } @@ -127,7 +138,16 @@ public void upload(String name, FormData data) { new MultipartFormDataHelper(requestOptions).serializeTextField("name", data.getName()) .serializeTextField("resolution", String.valueOf(data.getResolution())) .serializeTextField("type", Objects.toString(data.getType())).serializeJsonField("size", data.getSize()) - .serializeFileField("image", data.getImage(), data.getImageFilename()).end().getRequestBody(), + .serializeFileField("image", data.getImage().getContent(), data.getImage().getContentType(), + data.getImage().getFilename()) + .serializeFileFields("file", + data.getFile() == null ? null + : data.getFile().stream().map(FileDetails::getContent).collect(Collectors.toList()), + data.getFile() == null ? null + : data.getFile().stream().map(FileDetails::getContentType).collect(Collectors.toList()), + data.getFile() == null ? null + : data.getFile().stream().map(FileDetails::getFilename).collect(Collectors.toList())) + .end().getRequestBody(), requestOptions).getValue(); } } diff --git a/typespec-tests/src/main/java/com/cadl/multipart/implementation/MultipartFormDataHelper.java b/typespec-tests/src/main/java/com/cadl/multipart/implementation/MultipartFormDataHelper.java index c97892db67..c569e1658f 100644 --- a/typespec-tests/src/main/java/com/cadl/multipart/implementation/MultipartFormDataHelper.java +++ b/typespec-tests/src/main/java/com/cadl/multipart/implementation/MultipartFormDataHelper.java @@ -25,6 +25,8 @@ public final class MultipartFormDataHelper { */ private static final String CRLF = "\r\n"; + private static final String APPLICATION_OCTET_STREAM = "application/octet-stream"; + /** * Value to be used as part of the divider for the multipart requests. */ @@ -112,50 +114,56 @@ public MultipartFormDataHelper serializeJsonField(String fieldName, Object jsonO return this; } - // application/octet-stream /** - * Formats a application/octet-stream field for a multipart HTTP request. + * Formats a file field for a multipart HTTP request. * * @param fieldName the field name * @param file the BinaryData of the file + * @param contentType the content-type of the file * @param filename the filename * @return the MultipartFormDataHelper instance */ - public MultipartFormDataHelper serializeFileField(String fieldName, BinaryData file, String filename) { + public MultipartFormDataHelper serializeFileField(String fieldName, BinaryData file, String contentType, + String filename) { if (file != null) { + if (CoreUtils.isNullOrEmpty(contentType)) { + contentType = APPLICATION_OCTET_STREAM; + } if (CoreUtils.isNullOrEmpty(filename)) { filename = fieldName; } filename = normalizeAscii(filename); - writeFileField(fieldName, file, filename); + writeFileField(fieldName, file, contentType, filename); } return this; } - // application/octet-stream, multiple files /** - * Formats a application/octet-stream field (potentially multiple files) for a multipart HTTP request. + * Formats a file field (potentially multiple files) for a multipart HTTP request. * * @param fieldName the field name * @param files the List of BinaryData of the files - * @param filenames the List of filenames. - * If it is {@code null}, or the size of the List is smaller than that of "files", implementation-specific filename - * is used. + * @param contentTypes the List of content-type of the files + * @param filenames the List of filenames * @return the MultipartFormDataHelper instance */ public MultipartFormDataHelper serializeFileFields(String fieldName, List files, - List filenames) { + List contentTypes, List filenames) { if (files != null) { for (int i = 0; i < files.size(); ++i) { BinaryData file = files.get(i); - String filename = (filenames != null && filenames.size() > i) ? filenames.get(i) : null; + String contentType = contentTypes.get(i); + if (CoreUtils.isNullOrEmpty(contentType)) { + contentType = APPLICATION_OCTET_STREAM; + } + String filename = filenames.get(i); if (CoreUtils.isNullOrEmpty(filename)) { filename = fieldName + String.valueOf(i + 1); } filename = normalizeAscii(filename); - writeFileField(fieldName, file, filename); + writeFileField(fieldName, file, contentType, filename); } } return this; @@ -178,10 +186,10 @@ public MultipartFormDataHelper end() { return this; } - private void writeFileField(String fieldName, BinaryData file, String filename) { + private void writeFileField(String fieldName, BinaryData file, String contentType, String filename) { // Multipart preamble String fileFieldPreamble = partSeparator + CRLF + "Content-Disposition: form-data; name=\"" + fieldName - + "\"; filename=\"" + filename + "\"" + CRLF + "Content-Type: application/octet-stream" + CRLF + CRLF; + + "\"; filename=\"" + filename + "\"" + CRLF + "Content-Type: " + contentType + CRLF + CRLF; byte[] data = fileFieldPreamble.getBytes(encoderCharset); appendBytes(data); diff --git a/typespec-tests/src/main/java/com/cadl/multipart/models/FileDetails.java b/typespec-tests/src/main/java/com/cadl/multipart/models/FileDetails.java new file mode 100644 index 0000000000..f21c2fb119 --- /dev/null +++ b/typespec-tests/src/main/java/com/cadl/multipart/models/FileDetails.java @@ -0,0 +1,97 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +// Code generated by Microsoft (R) TypeSpec Code Generator. + +package com.cadl.multipart.models; + +import com.azure.core.annotation.Fluent; +import com.azure.core.annotation.Generated; +import com.azure.core.util.BinaryData; + +/** + * The file details model for the file. + */ +@Fluent +public final class FileDetails { + /* + * The content of the file + */ + @Generated + private final BinaryData content; + + /* + * The filename of the file + */ + @Generated + private String filename; + + /* + * The content-type of the file + */ + @Generated + private String contentType = "application/octet-stream"; + + /** + * Creates an instance of FileDetails class. + * + * @param content the content value to set. + */ + @Generated + public FileDetails(BinaryData content) { + this.content = content; + } + + /** + * Get the content property: The content of the file. + * + * @return the content value. + */ + @Generated + public BinaryData getContent() { + return this.content; + } + + /** + * Get the filename property: The filename of the file. + * + * @return the filename value. + */ + @Generated + public String getFilename() { + return this.filename; + } + + /** + * Set the filename property: The filename of the file. + * + * @param filename the filename value to set. + * @return the FileDetails object itself. + */ + @Generated + public FileDetails setFilename(String filename) { + this.filename = filename; + return this; + } + + /** + * Get the contentType property: The content-type of the file. + * + * @return the contentType value. + */ + @Generated + public String getContentType() { + return this.contentType; + } + + /** + * Set the contentType property: The content-type of the file. + * + * @param contentType the contentType value to set. + * @return the FileDetails object itself. + */ + @Generated + public FileDetails setContentType(String contentType) { + this.contentType = contentType; + return this; + } +} diff --git a/typespec-tests/src/main/java/com/cadl/multipart/models/FormData.java b/typespec-tests/src/main/java/com/cadl/multipart/models/FormData.java index 48382a956f..5e2df23775 100644 --- a/typespec-tests/src/main/java/com/cadl/multipart/models/FormData.java +++ b/typespec-tests/src/main/java/com/cadl/multipart/models/FormData.java @@ -6,7 +6,7 @@ import com.azure.core.annotation.Fluent; import com.azure.core.annotation.Generated; -import com.azure.core.util.BinaryData; +import java.util.List; /** * The FormData model. @@ -41,13 +41,13 @@ public final class FormData { * The image property. */ @Generated - private final BinaryData image; + private final ImageFileDetails image; /* - * The filename for image + * The file property. */ @Generated - private String imageFilename = "image"; + private List file; /** * Creates an instance of FormData class. @@ -59,7 +59,7 @@ public final class FormData { * @param image the image value to set. */ @Generated - public FormData(String name, int resolution, ImageType type, Size size, BinaryData image) { + public FormData(String name, int resolution, ImageType type, Size size, ImageFileDetails image) { this.name = name; this.resolution = resolution; this.type = type; @@ -113,29 +113,29 @@ public Size getSize() { * @return the image value. */ @Generated - public BinaryData getImage() { + public ImageFileDetails getImage() { return this.image; } /** - * Get the imageFilename property: The filename for image. + * Get the file property: The file property. * - * @return the imageFilename value. + * @return the file value. */ @Generated - public String getImageFilename() { - return this.imageFilename; + public List getFile() { + return this.file; } /** - * Set the imageFilename property: The filename for image. + * Set the file property: The file property. * - * @param imageFilename the imageFilename value to set. + * @param file the file value to set. * @return the FormData object itself. */ @Generated - public FormData setImageFilename(String imageFilename) { - this.imageFilename = imageFilename; + public FormData setFile(List file) { + this.file = file; return this; } } diff --git a/typespec-tests/src/main/java/com/cadl/multipart/models/ImageFileDetails.java b/typespec-tests/src/main/java/com/cadl/multipart/models/ImageFileDetails.java new file mode 100644 index 0000000000..5d90a6c39b --- /dev/null +++ b/typespec-tests/src/main/java/com/cadl/multipart/models/ImageFileDetails.java @@ -0,0 +1,97 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +// Code generated by Microsoft (R) TypeSpec Code Generator. + +package com.cadl.multipart.models; + +import com.azure.core.annotation.Fluent; +import com.azure.core.annotation.Generated; +import com.azure.core.util.BinaryData; + +/** + * The file details model for the image. + */ +@Fluent +public final class ImageFileDetails { + /* + * The content of the file + */ + @Generated + private final BinaryData content; + + /* + * The filename of the file + */ + @Generated + private String filename; + + /* + * The content-type of the file + */ + @Generated + private String contentType = "application/octet-stream"; + + /** + * Creates an instance of ImageFileDetails class. + * + * @param content the content value to set. + */ + @Generated + public ImageFileDetails(BinaryData content) { + this.content = content; + } + + /** + * Get the content property: The content of the file. + * + * @return the content value. + */ + @Generated + public BinaryData getContent() { + return this.content; + } + + /** + * Get the filename property: The filename of the file. + * + * @return the filename value. + */ + @Generated + public String getFilename() { + return this.filename; + } + + /** + * Set the filename property: The filename of the file. + * + * @param filename the filename value to set. + * @return the ImageFileDetails object itself. + */ + @Generated + public ImageFileDetails setFilename(String filename) { + this.filename = filename; + return this; + } + + /** + * Get the contentType property: The content-type of the file. + * + * @return the contentType value. + */ + @Generated + public String getContentType() { + return this.contentType; + } + + /** + * Set the contentType property: The content-type of the file. + * + * @param contentType the contentType value to set. + * @return the ImageFileDetails object itself. + */ + @Generated + public ImageFileDetails setContentType(String contentType) { + this.contentType = contentType; + return this; + } +} diff --git a/typespec-tests/src/main/java/com/payload/multipart/MultiPartAsyncClient.java b/typespec-tests/src/main/java/com/payload/multipart/MultiPartAsyncClient.java index 6fd584409b..ef97270d12 100644 --- a/typespec-tests/src/main/java/com/payload/multipart/MultiPartAsyncClient.java +++ b/typespec-tests/src/main/java/com/payload/multipart/MultiPartAsyncClient.java @@ -24,6 +24,8 @@ import com.payload.multipart.models.JsonPartRequest; import com.payload.multipart.models.MultiBinaryPartsRequest; import com.payload.multipart.models.MultiPartRequest; +import com.payload.multipart.models.PicturesFileDetails; +import java.util.stream.Collectors; import reactor.core.publisher.Mono; /** @@ -176,8 +178,9 @@ public Mono basic(MultiPartRequest body) { // Generated convenience method for basicWithResponse RequestOptions requestOptions = new RequestOptions(); return basicWithResponse(new MultipartFormDataHelper(requestOptions).serializeTextField("id", body.getId()) - .serializeFileField("profileImage", body.getProfileImage(), body.getProfileImageFilename()).end() - .getRequestBody(), requestOptions).flatMap(FluxUtil::toMono); + .serializeFileField("profileImage", body.getProfileImage().getContent(), + body.getProfileImage().getContentType(), body.getProfileImage().getFilename()) + .end().getRequestBody(), requestOptions).flatMap(FluxUtil::toMono); } /** @@ -199,10 +202,14 @@ public Mono complex(ComplexPartsRequest body) { RequestOptions requestOptions = new RequestOptions(); return complexWithResponse(new MultipartFormDataHelper(requestOptions).serializeTextField("id", body.getId()) .serializeJsonField("address", body.getAddress()) - .serializeFileField("profileImage", body.getProfileImage(), body.getProfileImageFilename()) + .serializeFileField("profileImage", body.getProfileImage().getContent(), + body.getProfileImage().getContentType(), body.getProfileImage().getFilename()) .serializeJsonField("previousAddresses", body.getPreviousAddresses()) - .serializeFileFields("pictures", body.getPictures(), body.getPicturesFilenames()).end().getRequestBody(), - requestOptions).flatMap(FluxUtil::toMono); + .serializeFileFields("pictures", + body.getPictures().stream().map(PicturesFileDetails::getContent).collect(Collectors.toList()), + body.getPictures().stream().map(PicturesFileDetails::getContentType).collect(Collectors.toList()), + body.getPictures().stream().map(PicturesFileDetails::getFilename).collect(Collectors.toList())) + .end().getRequestBody(), requestOptions).flatMap(FluxUtil::toMono); } /** @@ -222,11 +229,11 @@ public Mono complex(ComplexPartsRequest body) { public Mono jsonPart(JsonPartRequest body) { // Generated convenience method for jsonPartWithResponse RequestOptions requestOptions = new RequestOptions(); - return jsonPartWithResponse( - new MultipartFormDataHelper(requestOptions).serializeJsonField("address", body.getAddress()) - .serializeFileField("profileImage", body.getProfileImage(), body.getProfileImageFilename()).end() - .getRequestBody(), - requestOptions).flatMap(FluxUtil::toMono); + return jsonPartWithResponse(new MultipartFormDataHelper(requestOptions) + .serializeJsonField("address", body.getAddress()) + .serializeFileField("profileImage", body.getProfileImage().getContent(), + body.getProfileImage().getContentType(), body.getProfileImage().getFilename()) + .end().getRequestBody(), requestOptions).flatMap(FluxUtil::toMono); } /** @@ -246,9 +253,13 @@ public Mono jsonPart(JsonPartRequest body) { public Mono binaryArrayParts(BinaryArrayPartsRequest body) { // Generated convenience method for binaryArrayPartsWithResponse RequestOptions requestOptions = new RequestOptions(); - return binaryArrayPartsWithResponse(new MultipartFormDataHelper(requestOptions) - .serializeTextField("id", body.getId()) - .serializeFileFields("pictures", body.getPictures(), body.getPicturesFilenames()).end().getRequestBody(), + return binaryArrayPartsWithResponse( + new MultipartFormDataHelper(requestOptions).serializeTextField("id", body.getId()) + .serializeFileFields("pictures", + body.getPictures().stream().map(PicturesFileDetails::getContent).collect(Collectors.toList()), + body.getPictures().stream().map(PicturesFileDetails::getContentType).collect(Collectors.toList()), + body.getPictures().stream().map(PicturesFileDetails::getFilename).collect(Collectors.toList())) + .end().getRequestBody(), requestOptions).flatMap(FluxUtil::toMono); } @@ -271,7 +282,8 @@ public Mono jsonArrayParts(JsonArrayPartsRequest body) { RequestOptions requestOptions = new RequestOptions(); return jsonArrayPartsWithResponse( new MultipartFormDataHelper(requestOptions) - .serializeFileField("profileImage", body.getProfileImage(), body.getProfileImageFilename()) + .serializeFileField("profileImage", body.getProfileImage().getContent(), + body.getProfileImage().getContentType(), body.getProfileImage().getFilename()) .serializeJsonField("previousAddresses", body.getPreviousAddresses()).end().getRequestBody(), requestOptions).flatMap(FluxUtil::toMono); } @@ -295,8 +307,12 @@ public Mono multiBinaryParts(MultiBinaryPartsRequest body) { RequestOptions requestOptions = new RequestOptions(); return multiBinaryPartsWithResponse( new MultipartFormDataHelper(requestOptions) - .serializeFileField("profileImage", body.getProfileImage(), body.getProfileImageFilename()) - .serializeFileField("picture", body.getPicture(), body.getPictureFilename()).end().getRequestBody(), + .serializeFileField("profileImage", body.getProfileImage().getContent(), + body.getProfileImage().getContentType(), body.getProfileImage().getFilename()) + .serializeFileField("picture", body.getPicture() == null ? null : body.getPicture().getContent(), + body.getPicture() == null ? null : body.getPicture().getContentType(), + body.getPicture() == null ? null : body.getPicture().getFilename()) + .end().getRequestBody(), requestOptions).flatMap(FluxUtil::toMono); } } diff --git a/typespec-tests/src/main/java/com/payload/multipart/MultiPartClient.java b/typespec-tests/src/main/java/com/payload/multipart/MultiPartClient.java index e2a79c64ef..ee3cb8d82f 100644 --- a/typespec-tests/src/main/java/com/payload/multipart/MultiPartClient.java +++ b/typespec-tests/src/main/java/com/payload/multipart/MultiPartClient.java @@ -23,6 +23,8 @@ import com.payload.multipart.models.JsonPartRequest; import com.payload.multipart.models.MultiBinaryPartsRequest; import com.payload.multipart.models.MultiPartRequest; +import com.payload.multipart.models.PicturesFileDetails; +import java.util.stream.Collectors; /** * Initializes a new instance of the synchronous MultiPartClient type. @@ -173,8 +175,9 @@ public void basic(MultiPartRequest body) { // Generated convenience method for basicWithResponse RequestOptions requestOptions = new RequestOptions(); basicWithResponse(new MultipartFormDataHelper(requestOptions).serializeTextField("id", body.getId()) - .serializeFileField("profileImage", body.getProfileImage(), body.getProfileImageFilename()).end() - .getRequestBody(), requestOptions).getValue(); + .serializeFileField("profileImage", body.getProfileImage().getContent(), + body.getProfileImage().getContentType(), body.getProfileImage().getFilename()) + .end().getRequestBody(), requestOptions).getValue(); } /** @@ -195,10 +198,14 @@ public void complex(ComplexPartsRequest body) { RequestOptions requestOptions = new RequestOptions(); complexWithResponse(new MultipartFormDataHelper(requestOptions).serializeTextField("id", body.getId()) .serializeJsonField("address", body.getAddress()) - .serializeFileField("profileImage", body.getProfileImage(), body.getProfileImageFilename()) + .serializeFileField("profileImage", body.getProfileImage().getContent(), + body.getProfileImage().getContentType(), body.getProfileImage().getFilename()) .serializeJsonField("previousAddresses", body.getPreviousAddresses()) - .serializeFileFields("pictures", body.getPictures(), body.getPicturesFilenames()).end().getRequestBody(), - requestOptions).getValue(); + .serializeFileFields("pictures", + body.getPictures().stream().map(PicturesFileDetails::getContent).collect(Collectors.toList()), + body.getPictures().stream().map(PicturesFileDetails::getContentType).collect(Collectors.toList()), + body.getPictures().stream().map(PicturesFileDetails::getFilename).collect(Collectors.toList())) + .end().getRequestBody(), requestOptions).getValue(); } /** @@ -217,11 +224,11 @@ public void complex(ComplexPartsRequest body) { public void jsonPart(JsonPartRequest body) { // Generated convenience method for jsonPartWithResponse RequestOptions requestOptions = new RequestOptions(); - jsonPartWithResponse( - new MultipartFormDataHelper(requestOptions).serializeJsonField("address", body.getAddress()) - .serializeFileField("profileImage", body.getProfileImage(), body.getProfileImageFilename()).end() - .getRequestBody(), - requestOptions).getValue(); + jsonPartWithResponse(new MultipartFormDataHelper(requestOptions) + .serializeJsonField("address", body.getAddress()) + .serializeFileField("profileImage", body.getProfileImage().getContent(), + body.getProfileImage().getContentType(), body.getProfileImage().getFilename()) + .end().getRequestBody(), requestOptions).getValue(); } /** @@ -241,8 +248,11 @@ public void binaryArrayParts(BinaryArrayPartsRequest body) { // Generated convenience method for binaryArrayPartsWithResponse RequestOptions requestOptions = new RequestOptions(); binaryArrayPartsWithResponse(new MultipartFormDataHelper(requestOptions).serializeTextField("id", body.getId()) - .serializeFileFields("pictures", body.getPictures(), body.getPicturesFilenames()).end().getRequestBody(), - requestOptions).getValue(); + .serializeFileFields("pictures", + body.getPictures().stream().map(PicturesFileDetails::getContent).collect(Collectors.toList()), + body.getPictures().stream().map(PicturesFileDetails::getContentType).collect(Collectors.toList()), + body.getPictures().stream().map(PicturesFileDetails::getFilename).collect(Collectors.toList())) + .end().getRequestBody(), requestOptions).getValue(); } /** @@ -263,7 +273,8 @@ public void jsonArrayParts(JsonArrayPartsRequest body) { RequestOptions requestOptions = new RequestOptions(); jsonArrayPartsWithResponse( new MultipartFormDataHelper(requestOptions) - .serializeFileField("profileImage", body.getProfileImage(), body.getProfileImageFilename()) + .serializeFileField("profileImage", body.getProfileImage().getContent(), + body.getProfileImage().getContentType(), body.getProfileImage().getFilename()) .serializeJsonField("previousAddresses", body.getPreviousAddresses()).end().getRequestBody(), requestOptions).getValue(); } @@ -286,8 +297,12 @@ public void multiBinaryParts(MultiBinaryPartsRequest body) { RequestOptions requestOptions = new RequestOptions(); multiBinaryPartsWithResponse( new MultipartFormDataHelper(requestOptions) - .serializeFileField("profileImage", body.getProfileImage(), body.getProfileImageFilename()) - .serializeFileField("picture", body.getPicture(), body.getPictureFilename()).end().getRequestBody(), + .serializeFileField("profileImage", body.getProfileImage().getContent(), + body.getProfileImage().getContentType(), body.getProfileImage().getFilename()) + .serializeFileField("picture", body.getPicture() == null ? null : body.getPicture().getContent(), + body.getPicture() == null ? null : body.getPicture().getContentType(), + body.getPicture() == null ? null : body.getPicture().getFilename()) + .end().getRequestBody(), requestOptions).getValue(); } } diff --git a/typespec-tests/src/main/java/com/payload/multipart/implementation/MultipartFormDataHelper.java b/typespec-tests/src/main/java/com/payload/multipart/implementation/MultipartFormDataHelper.java index 289fe28f2c..8b1806e507 100644 --- a/typespec-tests/src/main/java/com/payload/multipart/implementation/MultipartFormDataHelper.java +++ b/typespec-tests/src/main/java/com/payload/multipart/implementation/MultipartFormDataHelper.java @@ -25,6 +25,8 @@ public final class MultipartFormDataHelper { */ private static final String CRLF = "\r\n"; + private static final String APPLICATION_OCTET_STREAM = "application/octet-stream"; + /** * Value to be used as part of the divider for the multipart requests. */ @@ -112,50 +114,56 @@ public MultipartFormDataHelper serializeJsonField(String fieldName, Object jsonO return this; } - // application/octet-stream /** - * Formats a application/octet-stream field for a multipart HTTP request. + * Formats a file field for a multipart HTTP request. * * @param fieldName the field name * @param file the BinaryData of the file + * @param contentType the content-type of the file * @param filename the filename * @return the MultipartFormDataHelper instance */ - public MultipartFormDataHelper serializeFileField(String fieldName, BinaryData file, String filename) { + public MultipartFormDataHelper serializeFileField(String fieldName, BinaryData file, String contentType, + String filename) { if (file != null) { + if (CoreUtils.isNullOrEmpty(contentType)) { + contentType = APPLICATION_OCTET_STREAM; + } if (CoreUtils.isNullOrEmpty(filename)) { filename = fieldName; } filename = normalizeAscii(filename); - writeFileField(fieldName, file, filename); + writeFileField(fieldName, file, contentType, filename); } return this; } - // application/octet-stream, multiple files /** - * Formats a application/octet-stream field (potentially multiple files) for a multipart HTTP request. + * Formats a file field (potentially multiple files) for a multipart HTTP request. * * @param fieldName the field name * @param files the List of BinaryData of the files - * @param filenames the List of filenames. - * If it is {@code null}, or the size of the List is smaller than that of "files", implementation-specific filename - * is used. + * @param contentTypes the List of content-type of the files + * @param filenames the List of filenames * @return the MultipartFormDataHelper instance */ public MultipartFormDataHelper serializeFileFields(String fieldName, List files, - List filenames) { + List contentTypes, List filenames) { if (files != null) { for (int i = 0; i < files.size(); ++i) { BinaryData file = files.get(i); - String filename = (filenames != null && filenames.size() > i) ? filenames.get(i) : null; + String contentType = contentTypes.get(i); + if (CoreUtils.isNullOrEmpty(contentType)) { + contentType = APPLICATION_OCTET_STREAM; + } + String filename = filenames.get(i); if (CoreUtils.isNullOrEmpty(filename)) { filename = fieldName + String.valueOf(i + 1); } filename = normalizeAscii(filename); - writeFileField(fieldName, file, filename); + writeFileField(fieldName, file, contentType, filename); } } return this; @@ -178,10 +186,10 @@ public MultipartFormDataHelper end() { return this; } - private void writeFileField(String fieldName, BinaryData file, String filename) { + private void writeFileField(String fieldName, BinaryData file, String contentType, String filename) { // Multipart preamble String fileFieldPreamble = partSeparator + CRLF + "Content-Disposition: form-data; name=\"" + fieldName - + "\"; filename=\"" + filename + "\"" + CRLF + "Content-Type: application/octet-stream" + CRLF + CRLF; + + "\"; filename=\"" + filename + "\"" + CRLF + "Content-Type: " + contentType + CRLF + CRLF; byte[] data = fileFieldPreamble.getBytes(encoderCharset); appendBytes(data); diff --git a/typespec-tests/src/main/java/com/payload/multipart/models/BinaryArrayPartsRequest.java b/typespec-tests/src/main/java/com/payload/multipart/models/BinaryArrayPartsRequest.java index 531ad001b3..e7d2b13e3d 100644 --- a/typespec-tests/src/main/java/com/payload/multipart/models/BinaryArrayPartsRequest.java +++ b/typespec-tests/src/main/java/com/payload/multipart/models/BinaryArrayPartsRequest.java @@ -4,15 +4,14 @@ package com.payload.multipart.models; -import com.azure.core.annotation.Fluent; import com.azure.core.annotation.Generated; -import com.azure.core.util.BinaryData; +import com.azure.core.annotation.Immutable; import java.util.List; /** * The BinaryArrayPartsRequest model. */ -@Fluent +@Immutable public final class BinaryArrayPartsRequest { /* * The id property. @@ -24,13 +23,7 @@ public final class BinaryArrayPartsRequest { * The pictures property. */ @Generated - private final List pictures; - - /* - * The filenames for pictures - */ - @Generated - private List picturesFilenames; + private final List pictures; /** * Creates an instance of BinaryArrayPartsRequest class. @@ -39,7 +32,7 @@ public final class BinaryArrayPartsRequest { * @param pictures the pictures value to set. */ @Generated - public BinaryArrayPartsRequest(String id, List pictures) { + public BinaryArrayPartsRequest(String id, List pictures) { this.id = id; this.pictures = pictures; } @@ -60,29 +53,7 @@ public String getId() { * @return the pictures value. */ @Generated - public List getPictures() { + public List getPictures() { return this.pictures; } - - /** - * Get the picturesFilenames property: The filenames for pictures. - * - * @return the picturesFilenames value. - */ - @Generated - public List getPicturesFilenames() { - return this.picturesFilenames; - } - - /** - * Set the picturesFilenames property: The filenames for pictures. - * - * @param picturesFilenames the picturesFilenames value to set. - * @return the BinaryArrayPartsRequest object itself. - */ - @Generated - public BinaryArrayPartsRequest setPicturesFilenames(List picturesFilenames) { - this.picturesFilenames = picturesFilenames; - return this; - } } diff --git a/typespec-tests/src/main/java/com/payload/multipart/models/ComplexPartsRequest.java b/typespec-tests/src/main/java/com/payload/multipart/models/ComplexPartsRequest.java index 24eca9278a..59e2ed120c 100644 --- a/typespec-tests/src/main/java/com/payload/multipart/models/ComplexPartsRequest.java +++ b/typespec-tests/src/main/java/com/payload/multipart/models/ComplexPartsRequest.java @@ -4,15 +4,14 @@ package com.payload.multipart.models; -import com.azure.core.annotation.Fluent; import com.azure.core.annotation.Generated; -import com.azure.core.util.BinaryData; +import com.azure.core.annotation.Immutable; import java.util.List; /** * The ComplexPartsRequest model. */ -@Fluent +@Immutable public final class ComplexPartsRequest { /* * The id property. @@ -30,13 +29,7 @@ public final class ComplexPartsRequest { * The profileImage property. */ @Generated - private final BinaryData profileImage; - - /* - * The filename for profileImage - */ - @Generated - private String profileImageFilename = "profileImage"; + private final ProfileImageFileDetails profileImage; /* * The previousAddresses property. @@ -48,13 +41,7 @@ public final class ComplexPartsRequest { * The pictures property. */ @Generated - private final List pictures; - - /* - * The filenames for pictures - */ - @Generated - private List picturesFilenames; + private final List pictures; /** * Creates an instance of ComplexPartsRequest class. @@ -66,8 +53,8 @@ public final class ComplexPartsRequest { * @param pictures the pictures value to set. */ @Generated - public ComplexPartsRequest(String id, Address address, BinaryData profileImage, List
previousAddresses, - List pictures) { + public ComplexPartsRequest(String id, Address address, ProfileImageFileDetails profileImage, + List
previousAddresses, List pictures) { this.id = id; this.address = address; this.profileImage = profileImage; @@ -101,32 +88,10 @@ public Address getAddress() { * @return the profileImage value. */ @Generated - public BinaryData getProfileImage() { + public ProfileImageFileDetails getProfileImage() { return this.profileImage; } - /** - * Get the profileImageFilename property: The filename for profileImage. - * - * @return the profileImageFilename value. - */ - @Generated - public String getProfileImageFilename() { - return this.profileImageFilename; - } - - /** - * Set the profileImageFilename property: The filename for profileImage. - * - * @param profileImageFilename the profileImageFilename value to set. - * @return the ComplexPartsRequest object itself. - */ - @Generated - public ComplexPartsRequest setProfileImageFilename(String profileImageFilename) { - this.profileImageFilename = profileImageFilename; - return this; - } - /** * Get the previousAddresses property: The previousAddresses property. * @@ -143,29 +108,7 @@ public List
getPreviousAddresses() { * @return the pictures value. */ @Generated - public List getPictures() { + public List getPictures() { return this.pictures; } - - /** - * Get the picturesFilenames property: The filenames for pictures. - * - * @return the picturesFilenames value. - */ - @Generated - public List getPicturesFilenames() { - return this.picturesFilenames; - } - - /** - * Set the picturesFilenames property: The filenames for pictures. - * - * @param picturesFilenames the picturesFilenames value to set. - * @return the ComplexPartsRequest object itself. - */ - @Generated - public ComplexPartsRequest setPicturesFilenames(List picturesFilenames) { - this.picturesFilenames = picturesFilenames; - return this; - } } diff --git a/typespec-tests/src/main/java/com/payload/multipart/models/JsonArrayPartsRequest.java b/typespec-tests/src/main/java/com/payload/multipart/models/JsonArrayPartsRequest.java index 7613945e29..6af4fba825 100644 --- a/typespec-tests/src/main/java/com/payload/multipart/models/JsonArrayPartsRequest.java +++ b/typespec-tests/src/main/java/com/payload/multipart/models/JsonArrayPartsRequest.java @@ -4,27 +4,20 @@ package com.payload.multipart.models; -import com.azure.core.annotation.Fluent; import com.azure.core.annotation.Generated; -import com.azure.core.util.BinaryData; +import com.azure.core.annotation.Immutable; import java.util.List; /** * The JsonArrayPartsRequest model. */ -@Fluent +@Immutable public final class JsonArrayPartsRequest { /* * The profileImage property. */ @Generated - private final BinaryData profileImage; - - /* - * The filename for profileImage - */ - @Generated - private String profileImageFilename = "profileImage"; + private final ProfileImageFileDetails profileImage; /* * The previousAddresses property. @@ -39,7 +32,7 @@ public final class JsonArrayPartsRequest { * @param previousAddresses the previousAddresses value to set. */ @Generated - public JsonArrayPartsRequest(BinaryData profileImage, List
previousAddresses) { + public JsonArrayPartsRequest(ProfileImageFileDetails profileImage, List
previousAddresses) { this.profileImage = profileImage; this.previousAddresses = previousAddresses; } @@ -50,32 +43,10 @@ public JsonArrayPartsRequest(BinaryData profileImage, List
previousAddr * @return the profileImage value. */ @Generated - public BinaryData getProfileImage() { + public ProfileImageFileDetails getProfileImage() { return this.profileImage; } - /** - * Get the profileImageFilename property: The filename for profileImage. - * - * @return the profileImageFilename value. - */ - @Generated - public String getProfileImageFilename() { - return this.profileImageFilename; - } - - /** - * Set the profileImageFilename property: The filename for profileImage. - * - * @param profileImageFilename the profileImageFilename value to set. - * @return the JsonArrayPartsRequest object itself. - */ - @Generated - public JsonArrayPartsRequest setProfileImageFilename(String profileImageFilename) { - this.profileImageFilename = profileImageFilename; - return this; - } - /** * Get the previousAddresses property: The previousAddresses property. * diff --git a/typespec-tests/src/main/java/com/payload/multipart/models/JsonPartRequest.java b/typespec-tests/src/main/java/com/payload/multipart/models/JsonPartRequest.java index 5c5f736e3a..3315968d3a 100644 --- a/typespec-tests/src/main/java/com/payload/multipart/models/JsonPartRequest.java +++ b/typespec-tests/src/main/java/com/payload/multipart/models/JsonPartRequest.java @@ -4,14 +4,13 @@ package com.payload.multipart.models; -import com.azure.core.annotation.Fluent; import com.azure.core.annotation.Generated; -import com.azure.core.util.BinaryData; +import com.azure.core.annotation.Immutable; /** * The JsonPartRequest model. */ -@Fluent +@Immutable public final class JsonPartRequest { /* * The address property. @@ -23,13 +22,7 @@ public final class JsonPartRequest { * The profileImage property. */ @Generated - private final BinaryData profileImage; - - /* - * The filename for profileImage - */ - @Generated - private String profileImageFilename = "profileImage"; + private final ProfileImageFileDetails profileImage; /** * Creates an instance of JsonPartRequest class. @@ -38,7 +31,7 @@ public final class JsonPartRequest { * @param profileImage the profileImage value to set. */ @Generated - public JsonPartRequest(Address address, BinaryData profileImage) { + public JsonPartRequest(Address address, ProfileImageFileDetails profileImage) { this.address = address; this.profileImage = profileImage; } @@ -59,29 +52,7 @@ public Address getAddress() { * @return the profileImage value. */ @Generated - public BinaryData getProfileImage() { + public ProfileImageFileDetails getProfileImage() { return this.profileImage; } - - /** - * Get the profileImageFilename property: The filename for profileImage. - * - * @return the profileImageFilename value. - */ - @Generated - public String getProfileImageFilename() { - return this.profileImageFilename; - } - - /** - * Set the profileImageFilename property: The filename for profileImage. - * - * @param profileImageFilename the profileImageFilename value to set. - * @return the JsonPartRequest object itself. - */ - @Generated - public JsonPartRequest setProfileImageFilename(String profileImageFilename) { - this.profileImageFilename = profileImageFilename; - return this; - } } diff --git a/typespec-tests/src/main/java/com/payload/multipart/models/MultiBinaryPartsRequest.java b/typespec-tests/src/main/java/com/payload/multipart/models/MultiBinaryPartsRequest.java index 17305b876a..3593588981 100644 --- a/typespec-tests/src/main/java/com/payload/multipart/models/MultiBinaryPartsRequest.java +++ b/typespec-tests/src/main/java/com/payload/multipart/models/MultiBinaryPartsRequest.java @@ -6,7 +6,6 @@ import com.azure.core.annotation.Fluent; import com.azure.core.annotation.Generated; -import com.azure.core.util.BinaryData; /** * The MultiBinaryPartsRequest model. @@ -17,25 +16,13 @@ public final class MultiBinaryPartsRequest { * The profileImage property. */ @Generated - private final BinaryData profileImage; - - /* - * The filename for profileImage - */ - @Generated - private String profileImageFilename = "profileImage"; + private final ProfileImageFileDetails profileImage; /* * The picture property. */ @Generated - private BinaryData picture; - - /* - * The filename for picture - */ - @Generated - private String pictureFilename = "picture"; + private PictureFileDetails picture; /** * Creates an instance of MultiBinaryPartsRequest class. @@ -43,7 +30,7 @@ public final class MultiBinaryPartsRequest { * @param profileImage the profileImage value to set. */ @Generated - public MultiBinaryPartsRequest(BinaryData profileImage) { + public MultiBinaryPartsRequest(ProfileImageFileDetails profileImage) { this.profileImage = profileImage; } @@ -53,39 +40,17 @@ public MultiBinaryPartsRequest(BinaryData profileImage) { * @return the profileImage value. */ @Generated - public BinaryData getProfileImage() { + public ProfileImageFileDetails getProfileImage() { return this.profileImage; } - /** - * Get the profileImageFilename property: The filename for profileImage. - * - * @return the profileImageFilename value. - */ - @Generated - public String getProfileImageFilename() { - return this.profileImageFilename; - } - - /** - * Set the profileImageFilename property: The filename for profileImage. - * - * @param profileImageFilename the profileImageFilename value to set. - * @return the MultiBinaryPartsRequest object itself. - */ - @Generated - public MultiBinaryPartsRequest setProfileImageFilename(String profileImageFilename) { - this.profileImageFilename = profileImageFilename; - return this; - } - /** * Get the picture property: The picture property. * * @return the picture value. */ @Generated - public BinaryData getPicture() { + public PictureFileDetails getPicture() { return this.picture; } @@ -96,30 +61,8 @@ public BinaryData getPicture() { * @return the MultiBinaryPartsRequest object itself. */ @Generated - public MultiBinaryPartsRequest setPicture(BinaryData picture) { + public MultiBinaryPartsRequest setPicture(PictureFileDetails picture) { this.picture = picture; return this; } - - /** - * Get the pictureFilename property: The filename for picture. - * - * @return the pictureFilename value. - */ - @Generated - public String getPictureFilename() { - return this.pictureFilename; - } - - /** - * Set the pictureFilename property: The filename for picture. - * - * @param pictureFilename the pictureFilename value to set. - * @return the MultiBinaryPartsRequest object itself. - */ - @Generated - public MultiBinaryPartsRequest setPictureFilename(String pictureFilename) { - this.pictureFilename = pictureFilename; - return this; - } } diff --git a/typespec-tests/src/main/java/com/payload/multipart/models/MultiPartRequest.java b/typespec-tests/src/main/java/com/payload/multipart/models/MultiPartRequest.java index 315da1e0ae..8a3f9877ff 100644 --- a/typespec-tests/src/main/java/com/payload/multipart/models/MultiPartRequest.java +++ b/typespec-tests/src/main/java/com/payload/multipart/models/MultiPartRequest.java @@ -4,14 +4,13 @@ package com.payload.multipart.models; -import com.azure.core.annotation.Fluent; import com.azure.core.annotation.Generated; -import com.azure.core.util.BinaryData; +import com.azure.core.annotation.Immutable; /** * The MultiPartRequest model. */ -@Fluent +@Immutable public final class MultiPartRequest { /* * The id property. @@ -23,13 +22,7 @@ public final class MultiPartRequest { * The profileImage property. */ @Generated - private final BinaryData profileImage; - - /* - * The filename for profileImage - */ - @Generated - private String profileImageFilename = "profileImage"; + private final ProfileImageFileDetails profileImage; /** * Creates an instance of MultiPartRequest class. @@ -38,7 +31,7 @@ public final class MultiPartRequest { * @param profileImage the profileImage value to set. */ @Generated - public MultiPartRequest(String id, BinaryData profileImage) { + public MultiPartRequest(String id, ProfileImageFileDetails profileImage) { this.id = id; this.profileImage = profileImage; } @@ -59,29 +52,7 @@ public String getId() { * @return the profileImage value. */ @Generated - public BinaryData getProfileImage() { + public ProfileImageFileDetails getProfileImage() { return this.profileImage; } - - /** - * Get the profileImageFilename property: The filename for profileImage. - * - * @return the profileImageFilename value. - */ - @Generated - public String getProfileImageFilename() { - return this.profileImageFilename; - } - - /** - * Set the profileImageFilename property: The filename for profileImage. - * - * @param profileImageFilename the profileImageFilename value to set. - * @return the MultiPartRequest object itself. - */ - @Generated - public MultiPartRequest setProfileImageFilename(String profileImageFilename) { - this.profileImageFilename = profileImageFilename; - return this; - } } diff --git a/typespec-tests/src/main/java/com/payload/multipart/models/PictureFileDetails.java b/typespec-tests/src/main/java/com/payload/multipart/models/PictureFileDetails.java new file mode 100644 index 0000000000..08283ff6ca --- /dev/null +++ b/typespec-tests/src/main/java/com/payload/multipart/models/PictureFileDetails.java @@ -0,0 +1,97 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +// Code generated by Microsoft (R) TypeSpec Code Generator. + +package com.payload.multipart.models; + +import com.azure.core.annotation.Fluent; +import com.azure.core.annotation.Generated; +import com.azure.core.util.BinaryData; + +/** + * The file details model for the picture. + */ +@Fluent +public final class PictureFileDetails { + /* + * The content of the file + */ + @Generated + private final BinaryData content; + + /* + * The filename of the file + */ + @Generated + private String filename; + + /* + * The content-type of the file + */ + @Generated + private String contentType = "application/octet-stream"; + + /** + * Creates an instance of PictureFileDetails class. + * + * @param content the content value to set. + */ + @Generated + public PictureFileDetails(BinaryData content) { + this.content = content; + } + + /** + * Get the content property: The content of the file. + * + * @return the content value. + */ + @Generated + public BinaryData getContent() { + return this.content; + } + + /** + * Get the filename property: The filename of the file. + * + * @return the filename value. + */ + @Generated + public String getFilename() { + return this.filename; + } + + /** + * Set the filename property: The filename of the file. + * + * @param filename the filename value to set. + * @return the PictureFileDetails object itself. + */ + @Generated + public PictureFileDetails setFilename(String filename) { + this.filename = filename; + return this; + } + + /** + * Get the contentType property: The content-type of the file. + * + * @return the contentType value. + */ + @Generated + public String getContentType() { + return this.contentType; + } + + /** + * Set the contentType property: The content-type of the file. + * + * @param contentType the contentType value to set. + * @return the PictureFileDetails object itself. + */ + @Generated + public PictureFileDetails setContentType(String contentType) { + this.contentType = contentType; + return this; + } +} diff --git a/typespec-tests/src/main/java/com/payload/multipart/models/PicturesFileDetails.java b/typespec-tests/src/main/java/com/payload/multipart/models/PicturesFileDetails.java new file mode 100644 index 0000000000..db43e77103 --- /dev/null +++ b/typespec-tests/src/main/java/com/payload/multipart/models/PicturesFileDetails.java @@ -0,0 +1,97 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +// Code generated by Microsoft (R) TypeSpec Code Generator. + +package com.payload.multipart.models; + +import com.azure.core.annotation.Fluent; +import com.azure.core.annotation.Generated; +import com.azure.core.util.BinaryData; + +/** + * The file details model for the pictures. + */ +@Fluent +public final class PicturesFileDetails { + /* + * The content of the file + */ + @Generated + private final BinaryData content; + + /* + * The filename of the file + */ + @Generated + private String filename; + + /* + * The content-type of the file + */ + @Generated + private String contentType = "application/octet-stream"; + + /** + * Creates an instance of PicturesFileDetails class. + * + * @param content the content value to set. + */ + @Generated + public PicturesFileDetails(BinaryData content) { + this.content = content; + } + + /** + * Get the content property: The content of the file. + * + * @return the content value. + */ + @Generated + public BinaryData getContent() { + return this.content; + } + + /** + * Get the filename property: The filename of the file. + * + * @return the filename value. + */ + @Generated + public String getFilename() { + return this.filename; + } + + /** + * Set the filename property: The filename of the file. + * + * @param filename the filename value to set. + * @return the PicturesFileDetails object itself. + */ + @Generated + public PicturesFileDetails setFilename(String filename) { + this.filename = filename; + return this; + } + + /** + * Get the contentType property: The content-type of the file. + * + * @return the contentType value. + */ + @Generated + public String getContentType() { + return this.contentType; + } + + /** + * Set the contentType property: The content-type of the file. + * + * @param contentType the contentType value to set. + * @return the PicturesFileDetails object itself. + */ + @Generated + public PicturesFileDetails setContentType(String contentType) { + this.contentType = contentType; + return this; + } +} diff --git a/typespec-tests/src/main/java/com/payload/multipart/models/ProfileImageFileDetails.java b/typespec-tests/src/main/java/com/payload/multipart/models/ProfileImageFileDetails.java new file mode 100644 index 0000000000..a26400954b --- /dev/null +++ b/typespec-tests/src/main/java/com/payload/multipart/models/ProfileImageFileDetails.java @@ -0,0 +1,97 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +// Code generated by Microsoft (R) TypeSpec Code Generator. + +package com.payload.multipart.models; + +import com.azure.core.annotation.Fluent; +import com.azure.core.annotation.Generated; +import com.azure.core.util.BinaryData; + +/** + * The file details model for the profileImage. + */ +@Fluent +public final class ProfileImageFileDetails { + /* + * The content of the file + */ + @Generated + private final BinaryData content; + + /* + * The filename of the file + */ + @Generated + private String filename; + + /* + * The content-type of the file + */ + @Generated + private String contentType = "application/octet-stream"; + + /** + * Creates an instance of ProfileImageFileDetails class. + * + * @param content the content value to set. + */ + @Generated + public ProfileImageFileDetails(BinaryData content) { + this.content = content; + } + + /** + * Get the content property: The content of the file. + * + * @return the content value. + */ + @Generated + public BinaryData getContent() { + return this.content; + } + + /** + * Get the filename property: The filename of the file. + * + * @return the filename value. + */ + @Generated + public String getFilename() { + return this.filename; + } + + /** + * Set the filename property: The filename of the file. + * + * @param filename the filename value to set. + * @return the ProfileImageFileDetails object itself. + */ + @Generated + public ProfileImageFileDetails setFilename(String filename) { + this.filename = filename; + return this; + } + + /** + * Get the contentType property: The content-type of the file. + * + * @return the contentType value. + */ + @Generated + public String getContentType() { + return this.contentType; + } + + /** + * Set the contentType property: The content-type of the file. + * + * @param contentType the contentType value to set. + * @return the ProfileImageFileDetails object itself. + */ + @Generated + public ProfileImageFileDetails setContentType(String contentType) { + this.contentType = contentType; + return this; + } +} diff --git a/typespec-tests/src/test/java/com/payload/multipart/MultipartTests.java b/typespec-tests/src/test/java/com/payload/multipart/MultipartTests.java index 763bbf167f..92b298c431 100644 --- a/typespec-tests/src/test/java/com/payload/multipart/MultipartTests.java +++ b/typespec-tests/src/test/java/com/payload/multipart/MultipartTests.java @@ -16,6 +16,9 @@ import com.payload.multipart.models.JsonPartRequest; import com.payload.multipart.models.MultiBinaryPartsRequest; import com.payload.multipart.models.MultiPartRequest; +import com.payload.multipart.models.PictureFileDetails; +import com.payload.multipart.models.PicturesFileDetails; +import com.payload.multipart.models.ProfileImageFileDetails; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Test; import reactor.core.publisher.Mono; @@ -24,7 +27,6 @@ import java.nio.file.Path; import java.util.ArrayList; import java.util.Arrays; -import java.util.Collections; import java.util.List; import java.util.regex.Matcher; import java.util.regex.Pattern; @@ -86,8 +88,10 @@ private static int[] computeFailure(byte[] pattern) { private final static class MultipartFilenameValidationPolicy implements HttpPipelinePolicy { private final List filenames = new ArrayList<>(); + private final List contentTypes = new ArrayList<>(); private final static Pattern FILENAME_PATTERN = Pattern.compile("filename=\"(.*?)\""); + private final static Pattern CONTENT_TYPE_PATTERN = Pattern.compile("Content-Type:\\s*(.*)"); @Override public Mono process(HttpPipelineCallContext context, HttpPipelineNextPolicy nextPolicy) { @@ -114,6 +118,25 @@ public Mono process(HttpPipelineCallContext context, HttpPipelineN start = posNewLine + 1; } + start = 0; + byte[] contentTypePattern = "Content-Type:".getBytes(StandardCharsets.UTF_8); + + while ((index = KpmAlgorithm.indexOf(body, start, stop, contentTypePattern)) >= 0) { + int posNewLine; + for (posNewLine = index; posNewLine < stop; ++posNewLine) { + if (body[posNewLine] == 10 || body[posNewLine] == 13) { + // newline + String line = new String(body, index, posNewLine - index, StandardCharsets.UTF_8); + Matcher matcher = CONTENT_TYPE_PATTERN.matcher(line); + if (matcher.find()) { + contentTypes.add(matcher.group(1)); + } + break; + } + } + start = posNewLine + 1; + } + // reset the body to compensate here consuming all the data context.getHttpRequest().setBody(body); return nextPolicy.process(); @@ -122,17 +145,21 @@ public Mono process(HttpPipelineCallContext context, HttpPipelineN private void validateFilenames(String... filenames) { Assertions.assertEquals(Arrays.asList(filenames), this.filenames); } + + private void validateContentTypes(String... contentTypes) { + Assertions.assertEquals(Arrays.asList(contentTypes), this.contentTypes); + } } @Test public void testBasic() { MultiPartRequest request = new MultiPartRequest( "123", - BinaryData.fromFile(FILE)); + new ProfileImageFileDetails(BinaryData.fromFile(FILE))); client.basic(request); - request.setProfileImageFilename("image.jpg"); + request.getProfileImage().setFilename("image.jpg"); asyncClient.basic(request).block(); } @@ -140,27 +167,27 @@ public void testBasic() { public void testJson() { client.jsonPart(new JsonPartRequest( new Address("X"), - BinaryData.fromFile(FILE))); + new ProfileImageFileDetails(BinaryData.fromFile(FILE)))); } @Test public void testJsonArray() { client.jsonArrayParts(new JsonArrayPartsRequest( - BinaryData.fromFile(FILE), - Arrays.asList(new Address("Y"), new Address("Z"))) - .setProfileImageFilename("image.jpg")); + new ProfileImageFileDetails(BinaryData.fromFile(FILE)).setFilename("image.jpg"), + Arrays.asList(new Address("Y"), new Address("Z")))); } @Test public void testMultipleFiles() { client.multiBinaryParts(new MultiBinaryPartsRequest( - BinaryData.fromFile(FILE)) - .setPicture(BinaryData.fromFile(FileUtils.getPngFile()))); + new ProfileImageFileDetails(BinaryData.fromFile(FILE))) + .setPicture(new PictureFileDetails(BinaryData.fromFile(FileUtils.getPngFile())))); validationPolicy.validateFilenames("profileImage", "picture"); // "picture" be optional - asyncClient.multiBinaryParts(new MultiBinaryPartsRequest(BinaryData.fromFile(FILE))).block(); + asyncClient.multiBinaryParts(new MultiBinaryPartsRequest( + new ProfileImageFileDetails(BinaryData.fromFile(FILE)))).block(); validationPolicy.validateFilenames("profileImage"); } @@ -170,14 +197,21 @@ public void testFileArray() { // provide no filename client.binaryArrayParts(new BinaryArrayPartsRequest( "123", - Arrays.asList(BinaryData.fromFile(PNG_FILE), BinaryData.fromFile(PNG_FILE)))); + Arrays.asList( + new PicturesFileDetails(BinaryData.fromFile(PNG_FILE)), + new PicturesFileDetails(BinaryData.fromFile(PNG_FILE)) + ))); + + validationPolicy.validateContentTypes("application/octet-stream", "application/octet-stream"); // provide only 1 filename, when there are 2 files // filename contains non-ASCII asyncClient.binaryArrayParts(new BinaryArrayPartsRequest( "123", - Arrays.asList(BinaryData.fromFile(PNG_FILE), BinaryData.fromFile(PNG_FILE))) - .setPicturesFilenames(Collections.singletonList("voilĂ .jpg"))).block(); + Arrays.asList( + new PicturesFileDetails(BinaryData.fromFile(PNG_FILE)).setFilename("voilĂ .jpg"), + new PicturesFileDetails(BinaryData.fromFile(PNG_FILE)) + ))).block(); validationPolicy.validateFilenames("voila.jpg", "pictures2"); } @@ -187,9 +221,12 @@ public void testComplex() { client.complex(new ComplexPartsRequest( "123", new Address("X"), - BinaryData.fromFile(FILE), + new ProfileImageFileDetails(BinaryData.fromFile(FILE)), Arrays.asList(new Address("Y"), new Address("Z")), - Arrays.asList(BinaryData.fromFile(PNG_FILE), BinaryData.fromFile(PNG_FILE)))); + Arrays.asList( + new PicturesFileDetails(BinaryData.fromFile(PNG_FILE)), + new PicturesFileDetails(BinaryData.fromFile(PNG_FILE)) + ))); validationPolicy.validateFilenames("profileImage", "pictures1", "pictures2"); @@ -197,11 +234,13 @@ public void testComplex() { asyncClient.complex(new ComplexPartsRequest( "123", new Address("X"), - BinaryData.fromFile(FILE), + new ProfileImageFileDetails(BinaryData.fromFile(FILE)), Arrays.asList(new Address("Y"), new Address("Z")), - Arrays.asList(BinaryData.fromFile(PNG_FILE), BinaryData.fromFile(PNG_FILE))) - .setPicturesFilenames(Arrays.asList("picture1", "picture2", "picture3"))).block(); + Arrays.asList( + new PicturesFileDetails(BinaryData.fromFile(PNG_FILE)), + new PicturesFileDetails(BinaryData.fromFile(PNG_FILE)).setFilename("picture2") + ))).block(); - validationPolicy.validateFilenames("profileImage", "picture1", "picture2"); + validationPolicy.validateFilenames("profileImage", "pictures1", "picture2"); } } diff --git a/typespec-tests/tsp/multipart.tsp b/typespec-tests/tsp/multipart.tsp index e92f49ebb8..c9eea8c763 100644 --- a/typespec-tests/tsp/multipart.tsp +++ b/typespec-tests/tsp/multipart.tsp @@ -23,6 +23,7 @@ model FormData { type: ImageType; size: Size; image: bytes; + file?: bytes[]; } @doc("request is binary")