Skip to content

Commit

Permalink
tsp, multipart with ##FileDetails class (#2525)
Browse files Browse the repository at this point in the history
  • Loading branch information
weidongxu-microsoft committed Jan 26, 2024
1 parent 28d35e5 commit 4f48009
Show file tree
Hide file tree
Showing 33 changed files with 952 additions and 461 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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<ClientModel> clientModels = autoRestModelTypes
List<ClientModel> 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<ClientModel> clientModels = Stream.concat(clientModelsFromCodeModel.stream(), ClientModels.getInstance().getModels().stream())
.distinct()
.collect(Collectors.toList());
builder.models(clientModels);

// union model (class)
Expand Down
45 changes: 16 additions & 29 deletions javagen/src/main/java/com/azure/autorest/mapper/ModelMapper.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -586,45 +587,31 @@ private static String disambiguatePropertyNameOfFlattenedSchema(Set<String> prop
return ret;
}

private static void processMultipartFormDataProperties(List<ClientModelProperty> properties) {
private static void processMultipartFormDataProperties(ObjectSchema compositeType, List<ClientModelProperty> 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<ClientModelProperty> 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<byte[]> with List<BinaryData>
IType fileDetailsModelType = ClientModelUtil.getMultipartFileDetailsModel(compositeType, property.getName());
// replace List<byte[]> with List<ClientModel>
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<String>
// 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());
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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);
}
}
Expand Down Expand Up @@ -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<String, Optional<ClientModelProperty>> findFileNameProperty =
(serializedName) -> model.getProperties().stream()
// here is a hack to find matching filename property by finding property of type String/List<String> 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<String, String, String> nullableExpression = (propertyExpr, expr) -> propertyExpr + " == null ? null : " + expr;

// serialize model for multipart/form-data
StringBuilder builder = new StringBuilder().append("new MultipartFormDataHelper(requestOptions)");
Set<String> 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);
Expand All @@ -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<MethodParameter, MethodParameter> findParametersForConvenienceMethod(
ClientMethod convenienceMethod, ClientMethod protocolMethod) {
Map<MethodParameter, MethodParameter> parameterMap = new LinkedHashMap<>();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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);
}

/**
Expand Down
Loading

0 comments on commit 4f48009

Please sign in to comment.