diff --git a/sdk-platform-java/gapic-generator-java/src/main/java/com/google/api/generator/gapic/composer/rest/HttpJsonDescriptorComposer.java b/sdk-platform-java/gapic-generator-java/src/main/java/com/google/api/generator/gapic/composer/rest/HttpJsonDescriptorComposer.java new file mode 100644 index 000000000000..71e38aa5829b --- /dev/null +++ b/sdk-platform-java/gapic-generator-java/src/main/java/com/google/api/generator/gapic/composer/rest/HttpJsonDescriptorComposer.java @@ -0,0 +1,675 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package com.google.api.generator.gapic.composer.rest; + +import com.google.api.core.InternalApi; +import com.google.api.gax.httpjson.ApiMethodDescriptor; +import com.google.api.gax.httpjson.ApiMethodDescriptor.MethodType; +import com.google.api.gax.httpjson.FieldsExtractor; +import com.google.api.gax.httpjson.ProtoMessageRequestFormatter; +import com.google.api.gax.httpjson.ProtoMessageResponseParser; +import com.google.api.gax.httpjson.ProtoRestSerializer; +import com.google.api.generator.engine.ast.AnnotationNode; +import com.google.api.generator.engine.ast.AssignmentExpr; +import com.google.api.generator.engine.ast.ConcreteReference; +import com.google.api.generator.engine.ast.EnumRefExpr; +import com.google.api.generator.engine.ast.Expr; +import com.google.api.generator.engine.ast.ExprStatement; +import com.google.api.generator.engine.ast.IfStatement; +import com.google.api.generator.engine.ast.LambdaExpr; +import com.google.api.generator.engine.ast.MethodDefinition; +import com.google.api.generator.engine.ast.MethodInvocationExpr; +import com.google.api.generator.engine.ast.NewObjectExpr; +import com.google.api.generator.engine.ast.PrimitiveValue; +import com.google.api.generator.engine.ast.ScopeNode; +import com.google.api.generator.engine.ast.Statement; +import com.google.api.generator.engine.ast.StringObjectValue; +import com.google.api.generator.engine.ast.TypeNode; +import com.google.api.generator.engine.ast.ValueExpr; +import com.google.api.generator.engine.ast.Variable; +import com.google.api.generator.engine.ast.VariableExpr; +import com.google.api.generator.gapic.composer.store.TypeStore; +import com.google.api.generator.gapic.model.HttpBindings.HttpBinding; +import com.google.api.generator.gapic.model.Method; +import com.google.api.generator.gapic.model.Service; +import com.google.api.generator.gapic.utils.JavaStyle; +import com.google.common.base.Strings; +import com.google.common.collect.ImmutableList; +import com.google.protobuf.TypeRegistry; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.function.BiFunction; +import java.util.function.Function; +import java.util.stream.Collectors; +import org.jspecify.annotations.NullMarked; +import org.jspecify.annotations.Nullable; + +@NullMarked +final class HttpJsonDescriptorComposer { + + private static final String BUILD_METHOD_NAME = "build"; + private static final String NEW_BUILDER_METHOD_NAME = "newBuilder"; + private static final String SLASH = "/"; + + private static final TypeStore FIXED_REST_TYPESTORE = createStaticTypes(); + + private static final VariableExpr TYPE_REGISTRY_VAR_EXPR = + VariableExpr.builder() + .setVariable( + Variable.builder() + .setName("typeRegistry") + .setType(FIXED_REST_TYPESTORE.get(TypeRegistry.class.getSimpleName())) + .build()) + .build(); + + private HttpJsonDescriptorComposer() {} + + private static TypeStore createStaticTypes() { + return new TypeStore( + Arrays.asList( + ApiMethodDescriptor.class, + ArrayList.class, + FieldsExtractor.class, + HashMap.class, + InternalApi.class, + List.class, + ProtoMessageRequestFormatter.class, + ProtoMessageResponseParser.class, + ProtoRestSerializer.class, + TypeRegistry.class)); + } + + static BiFunction, Function> + getMethodMaker() { + return (mName, argExpr) -> + m -> + MethodInvocationExpr.builder() + .setMethodName(mName) + .setArguments(argExpr) + .setExprReferenceExpr(m) + .build(); + } + + static String prependPathPrefix(String path, @Nullable String pathPrefix) { + if (pathPrefix == null) { + return path; + } + String normalizedPrefix = pathPrefix.startsWith(SLASH) ? pathPrefix : SLASH + pathPrefix; + if (normalizedPrefix.endsWith(SLASH)) { + normalizedPrefix = normalizedPrefix.substring(0, normalizedPrefix.length() - 1); + } + return normalizedPrefix + (path.startsWith(SLASH) ? path : SLASH + path); + } + + static String getProtoRpcFullMethodName(Service protoService, Method protoMethod) { + if (protoMethod.isMixin()) { + return String.format("%s/%s", protoMethod.mixedInApiName(), protoMethod.name()); + } + return String.format( + "%s.%s/%s", protoService.protoPakkage(), protoService.name(), protoMethod.name()); + } + + static List getHttpMethodTypeExpr(Method protoMethod) { + return Collections.singletonList( + ValueExpr.withValue( + StringObjectValue.withValue(protoMethod.httpBindings().httpVerb().toString()))); + } + + static List getMethodTypeExpr(Method protoMethod) { + MethodType methodType; + switch (protoMethod.stream()) { + case NONE: + methodType = MethodType.UNARY; + break; + case SERVER: + methodType = MethodType.SERVER_STREAMING; + break; + case CLIENT: + case BIDI: + default: + throw new UnsupportedOperationException( + String.format( + "Methods of type %s are not supported by REST transport", protoMethod.stream())); + } + EnumRefExpr expr = + EnumRefExpr.builder() + .setName(methodType.toString()) + .setType( + TypeNode.withReference( + ConcreteReference.builder() + .setClazz(ApiMethodDescriptor.MethodType.class) + .build())) + .build(); + return Collections.singletonList(expr); + } + + static List getRequestFormatterExpr(Method protoMethod, boolean restNumericEnumsEnabled) { + return getRequestFormatterExpr(protoMethod, restNumericEnumsEnabled, null); + } + + static List getRequestFormatterExpr( + Method protoMethod, boolean restNumericEnumsEnabled, @Nullable String pathPrefix) { + BiFunction, Function> + methodMaker = getMethodMaker(); + + MethodInvocationExpr expr = + MethodInvocationExpr.builder() + .setStaticReferenceType( + FIXED_REST_TYPESTORE.get(ProtoMessageRequestFormatter.class.getSimpleName())) + .setMethodName(NEW_BUILDER_METHOD_NAME) + .setGenerics(Collections.singletonList(protoMethod.inputType().reference())) + .build(); + + TypeNode extractorVarType = + TypeNode.withReference( + ConcreteReference.builder() + .setClazz(Map.class) + .setGenerics(TypeNode.STRING.reference(), TypeNode.STRING.reference()) + .build()); + + String pathPattern = + prependPathPrefix(protoMethod.httpBindings().lowerCamelPattern(), pathPrefix); + + expr = + methodMaker + .apply( + "setPath", + Arrays.asList( + ValueExpr.withValue(StringObjectValue.withValue(pathPattern)), + createFieldsExtractorClassInstance( + protoMethod, + extractorVarType, + protoMethod.httpBindings().pathParameters(), + "putPathParam", + restNumericEnumsEnabled))) + .apply(expr); + + if (!protoMethod.httpBindings().lowerCamelAdditionalPatterns().isEmpty()) { + expr = + methodMaker + .apply( + "setAdditionalPaths", + protoMethod.httpBindings().lowerCamelAdditionalPatterns().stream() + .map( + a -> + ValueExpr.withValue( + StringObjectValue.withValue(prependPathPrefix(a, pathPrefix)))) + .collect(Collectors.toList())) + .apply(expr); + } + TypeNode fieldsVarGenericType = + TypeNode.withReference( + ConcreteReference.builder() + .setClazz(List.class) + .setGenerics(TypeNode.STRING.reference()) + .build()); + + extractorVarType = + TypeNode.withReference( + ConcreteReference.builder() + .setClazz(Map.class) + .setGenerics(TypeNode.STRING.reference(), fieldsVarGenericType.reference()) + .build()); + + expr = + methodMaker + .apply( + "setQueryParamsExtractor", + Arrays.asList( + createFieldsExtractorClassInstance( + protoMethod, + extractorVarType, + protoMethod.httpBindings().queryParameters(), + "putQueryParam", + restNumericEnumsEnabled))) + .apply(expr); + + extractorVarType = TypeNode.STRING; + boolean asteriskBody = protoMethod.httpBindings().isAsteriskBody(); + expr = + methodMaker + .apply( + "setRequestBodyExtractor", + Arrays.asList( + createBodyFieldsExtractorClassInstance( + protoMethod, + extractorVarType, + asteriskBody + ? protoMethod.httpBindings().pathParameters() + : protoMethod.httpBindings().bodyParameters(), + "toBody", + asteriskBody, + restNumericEnumsEnabled))) + .apply(expr); + + expr = methodMaker.apply(BUILD_METHOD_NAME, Collections.emptyList()).apply(expr); + + return Collections.singletonList(expr); + } + + static List setResponseParserExpr(Method protoMethod) { + BiFunction, Function> + methodMaker = getMethodMaker(); + + MethodInvocationExpr expr = + MethodInvocationExpr.builder() + .setStaticReferenceType( + FIXED_REST_TYPESTORE.get(ProtoMessageResponseParser.class.getSimpleName())) + .setMethodName(NEW_BUILDER_METHOD_NAME) + .setGenerics(Collections.singletonList(protoMethod.outputType().reference())) + .build(); + + expr = + methodMaker + .apply( + "setDefaultInstance", + Arrays.asList( + MethodInvocationExpr.builder() + .setStaticReferenceType(protoMethod.outputType()) + .setMethodName("getDefaultInstance") + .setReturnType(protoMethod.outputType()) + .build())) + .apply(expr); + + expr = + methodMaker + .apply("setDefaultTypeRegistry", Arrays.asList(TYPE_REGISTRY_VAR_EXPR)) + .apply(expr); + expr = methodMaker.apply(BUILD_METHOD_NAME, Collections.emptyList()).apply(expr); + + return Collections.singletonList(expr); + } + + static MethodInvocationExpr createMethodDescriptorExpr( + Service service, + Method protoMethod, + VariableExpr methodDescriptorVarExpr, + boolean restNumericEnumsEnabled, + @Nullable String pathPrefix) { + MethodInvocationExpr expr = + MethodInvocationExpr.builder() + .setMethodName(NEW_BUILDER_METHOD_NAME) + .setStaticReferenceType( + FIXED_REST_TYPESTORE.get(ApiMethodDescriptor.class.getSimpleName())) + .setGenerics(methodDescriptorVarExpr.variable().type().reference().generics()) + .build(); + + BiFunction, Function> + methodMaker = getMethodMaker(); + + String codeMethodArgName = getProtoRpcFullMethodName(service, protoMethod); + expr = + methodMaker + .apply( + "setFullMethodName", + Arrays.asList(ValueExpr.withValue(StringObjectValue.withValue(codeMethodArgName)))) + .apply(expr); + + expr = methodMaker.apply("setHttpMethod", getHttpMethodTypeExpr(protoMethod)).apply(expr); + expr = methodMaker.apply("setType", getMethodTypeExpr(protoMethod)).apply(expr); + expr = + methodMaker + .apply( + "setRequestFormatter", + getRequestFormatterExpr(protoMethod, restNumericEnumsEnabled, pathPrefix)) + .apply(expr); + expr = methodMaker.apply("setResponseParser", setResponseParserExpr(protoMethod)).apply(expr); + + return expr; + } + + static Statement createMethodDescriptorVariableDecl( + Service service, + Method protoMethod, + VariableExpr methodDescriptorVarExpr, + boolean restNumericEnumsEnabled, + @Nullable String pathPrefix) { + MethodInvocationExpr expr = + createMethodDescriptorExpr( + service, protoMethod, methodDescriptorVarExpr, restNumericEnumsEnabled, pathPrefix); + + expr = + MethodInvocationExpr.builder() + .setMethodName(BUILD_METHOD_NAME) + .setExprReferenceExpr(expr) + .setReturnType(methodDescriptorVarExpr.type()) + .build(); + + return ExprStatement.withExpr( + AssignmentExpr.builder() + .setVariableExpr( + methodDescriptorVarExpr.toBuilder() + .setIsDecl(true) + .setScope(ScopeNode.PRIVATE) + .setIsStatic(true) + .setIsFinal(true) + .build()) + .setValueExpr(expr) + .build()); + } + + static MethodDefinition createGetMethodDescriptorsMethod( + Collection methodDescriptorVarExprs) { + List bodyExprs = new ArrayList<>(); + + VariableExpr methodDescriptorsVarExpr = + VariableExpr.withVariable( + Variable.builder() + .setType( + TypeNode.withReference( + ConcreteReference.builder() + .setClazz(List.class) + .setGenerics( + Arrays.asList( + FIXED_REST_TYPESTORE.get("ApiMethodDescriptor").reference())) + .build())) + .setName("methodDescriptors") + .build()); + + bodyExprs.add( + AssignmentExpr.builder() + .setVariableExpr(methodDescriptorsVarExpr.toBuilder().setIsDecl(true).build()) + .setValueExpr( + NewObjectExpr.builder() + .setType(FIXED_REST_TYPESTORE.get("ArrayList")) + .setIsGeneric(true) + .build()) + .build()); + + for (VariableExpr methodDescriptorVarExpr : methodDescriptorVarExprs) { + bodyExprs.add( + MethodInvocationExpr.builder() + .setExprReferenceExpr(methodDescriptorsVarExpr) + .setMethodName("add") + .setArguments(methodDescriptorVarExpr) + .build()); + } + + return MethodDefinition.builder() + .setScope(ScopeNode.PUBLIC) + .setIsStatic(true) + .setReturnType(methodDescriptorsVarExpr.type()) + .setReturnExpr(methodDescriptorsVarExpr) + .setAnnotations( + Arrays.asList( + AnnotationNode.withType( + TypeNode.withReference(ConcreteReference.withClazz(InternalApi.class))))) + .setName("getMethodDescriptors") + .setBody(bodyExprs.stream().map(ExprStatement::withExpr).collect(Collectors.toList())) + .build(); + } + + private static Expr createBodyFieldsExtractorClassInstance( + Method method, + TypeNode extractorReturnType, + Set httpBindingFieldNames, + String serializerMethodName, + boolean asteriskBody, + boolean restNumericEnumEnabled) { + List bodyStatements = new ArrayList<>(); + + Expr returnExpr = null; + Expr serializerExpr = + MethodInvocationExpr.builder() + .setMethodName("create") + .setStaticReferenceType( + FIXED_REST_TYPESTORE.get(ProtoRestSerializer.class.getSimpleName())) + .build(); + + VariableExpr requestVarExpr = + VariableExpr.withVariable( + Variable.builder().setType(method.inputType()).setName("request").build()); + Expr bodyRequestExpr = requestVarExpr; + String requestMethodPrefix = "get"; + String bodyParamName = null; + + if (asteriskBody) { + bodyRequestExpr = + MethodInvocationExpr.builder() + .setExprReferenceExpr(requestVarExpr) + .setMethodName("toBuilder") + .build(); + requestMethodPrefix = "clear"; + } + + Expr prevExpr = bodyRequestExpr; + for (HttpBinding httpBindingFieldName : httpBindingFieldNames) { + MethodInvocationExpr.Builder requestFieldMethodExprBuilder = + MethodInvocationExpr.builder().setExprReferenceExpr(prevExpr); + bodyParamName = + !Strings.isNullOrEmpty(httpBindingFieldName.jsonName()) + ? httpBindingFieldName.jsonName() + : JavaStyle.toLowerCamelCase(httpBindingFieldName.name()); + String[] descendantFields = httpBindingFieldName.name().split("\\."); + if (asteriskBody && descendantFields.length > 1) { + continue; + } + + for (int i = 0; i < descendantFields.length; i++) { + String currFieldName = descendantFields[i]; + String bindingFieldMethodName = + String.format("%s%s", requestMethodPrefix, JavaStyle.toUpperCamelCase(currFieldName)); + requestFieldMethodExprBuilder = + requestFieldMethodExprBuilder.setMethodName(bindingFieldMethodName); + + if (i < descendantFields.length - 1) { + requestFieldMethodExprBuilder = + MethodInvocationExpr.builder() + .setExprReferenceExpr(requestFieldMethodExprBuilder.build()); + } + } + prevExpr = requestFieldMethodExprBuilder.build(); + } + + if (httpBindingFieldNames.isEmpty() && !asteriskBody) { + returnExpr = ValueExpr.createNullExpr(); + } else { + ImmutableList.Builder paramsPutArgs = ImmutableList.builder(); + if (asteriskBody) { + prevExpr = + MethodInvocationExpr.builder() + .setExprReferenceExpr(prevExpr) + .setMethodName(BUILD_METHOD_NAME) + .build(); + bodyParamName = "*"; + } + paramsPutArgs.add(ValueExpr.withValue(StringObjectValue.withValue(bodyParamName))); + paramsPutArgs.add(prevExpr); + + PrimitiveValue primitiveValue = + PrimitiveValue.builder() + .setType(TypeNode.BOOLEAN) + .setValue(String.valueOf(restNumericEnumEnabled)) + .build(); + paramsPutArgs.add(ValueExpr.withValue(primitiveValue)); + + returnExpr = + MethodInvocationExpr.builder() + .setExprReferenceExpr(serializerExpr) + .setMethodName(serializerMethodName) + .setArguments(paramsPutArgs.build()) + .setReturnType(extractorReturnType) + .build(); + } + + return LambdaExpr.builder() + .setArguments(requestVarExpr.toBuilder().setIsDecl(true).build()) + .setBody(bodyStatements) + .setReturnExpr(returnExpr) + .build(); + } + + private static Expr createFieldsExtractorClassInstance( + Method method, + TypeNode extractorReturnType, + Set httpBindingFieldNames, + String serializerMethodName, + boolean restNumericEnumsEnabled) { + List bodyStatements = new ArrayList<>(); + + VariableExpr fieldsVarExpr = + VariableExpr.withVariable( + Variable.builder().setName("fields").setType(extractorReturnType).build()); + Expr fieldsAssignExpr = + AssignmentExpr.builder() + .setVariableExpr(fieldsVarExpr.toBuilder().setIsDecl(true).build()) + .setValueExpr( + NewObjectExpr.builder() + .setType(FIXED_REST_TYPESTORE.get(HashMap.class.getSimpleName())) + .setIsGeneric(true) + .build()) + .build(); + + bodyStatements.add(ExprStatement.withExpr(fieldsAssignExpr)); + + TypeNode serializerVarType = + TypeNode.withReference( + ConcreteReference.builder() + .setClazz(ProtoRestSerializer.class) + .setGenerics(method.inputType().reference()) + .build()); + + VariableExpr serializerVarExpr = + VariableExpr.withVariable( + Variable.builder().setName("serializer").setType(serializerVarType).build()); + + Expr serializerAssignExpr = + AssignmentExpr.builder() + .setVariableExpr(serializerVarExpr.toBuilder().setIsDecl(true).build()) + .setValueExpr( + MethodInvocationExpr.builder() + .setStaticReferenceType( + FIXED_REST_TYPESTORE.get(ProtoRestSerializer.class.getSimpleName())) + .setMethodName("create") + .setReturnType(serializerVarType) + .build()) + .build(); + + bodyStatements.add(ExprStatement.withExpr(serializerAssignExpr)); + + VariableExpr requestVarExpr = + VariableExpr.withVariable( + Variable.builder().setType(method.inputType()).setName("request").build()); + + for (HttpBinding httpBindingFieldName : httpBindingFieldNames) { + MethodInvocationExpr.Builder requestFieldGetterExprBuilder = + MethodInvocationExpr.builder().setExprReferenceExpr(requestVarExpr); + MethodInvocationExpr.Builder requestFieldHasExprBuilder = + MethodInvocationExpr.builder().setExprReferenceExpr(requestVarExpr); + String[] descendantFields = httpBindingFieldName.name().split("\\."); + for (int i = 0; i < descendantFields.length; i++) { + String currFieldName = descendantFields[i]; + String bindingFieldMethodName = + getBindingFieldMethodName( + httpBindingFieldName, + descendantFields.length, + i, + JavaStyle.toUpperCamelCase(currFieldName)); + requestFieldGetterExprBuilder = + requestFieldGetterExprBuilder.setMethodName(bindingFieldMethodName); + + String bindingFieldHasMethodName = + (i < descendantFields.length - 1) + ? bindingFieldMethodName + : String.format("has%s", JavaStyle.toUpperCamelCase(currFieldName)); + requestFieldHasExprBuilder = + requestFieldHasExprBuilder + .setMethodName(bindingFieldHasMethodName) + .setReturnType(TypeNode.BOOLEAN); + + if (i < descendantFields.length - 1) { + requestFieldGetterExprBuilder = + MethodInvocationExpr.builder() + .setExprReferenceExpr(requestFieldGetterExprBuilder.build()); + requestFieldHasExprBuilder = + MethodInvocationExpr.builder() + .setExprReferenceExpr(requestFieldHasExprBuilder.build()); + } + } + + MethodInvocationExpr requestBuilderExpr = requestFieldGetterExprBuilder.build(); + MethodInvocationExpr requestHasExpr = requestFieldHasExprBuilder.build(); + + ImmutableList.Builder paramsPutArgs = ImmutableList.builder(); + paramsPutArgs.add(fieldsVarExpr); + paramsPutArgs.add( + ValueExpr.withValue( + StringObjectValue.withValue( + (httpBindingFieldName.jsonName() != null) + ? httpBindingFieldName.jsonName() + : JavaStyle.toLowerCamelCase(httpBindingFieldName.name())))); + paramsPutArgs.add(requestBuilderExpr); + + Expr paramsPutExpr = + MethodInvocationExpr.builder() + .setExprReferenceExpr(serializerVarExpr) + .setMethodName(serializerMethodName) + .setArguments(paramsPutArgs.build()) + .setReturnType(extractorReturnType) + .build(); + + if (httpBindingFieldName.isOptional()) { + bodyStatements.add( + IfStatement.builder() + .setConditionExpr(requestHasExpr) + .setBody(Arrays.asList(ExprStatement.withExpr(paramsPutExpr))) + .build()); + } else { + bodyStatements.add(ExprStatement.withExpr(paramsPutExpr)); + } + } + + if (restNumericEnumsEnabled && serializerMethodName.equals("putQueryParam")) { + ImmutableList.Builder paramsPutArgs = ImmutableList.builder(); + paramsPutArgs.add(fieldsVarExpr); + paramsPutArgs.add(ValueExpr.withValue(StringObjectValue.withValue("$alt"))); + paramsPutArgs.add(ValueExpr.withValue(StringObjectValue.withValue("json;enum-encoding=int"))); + + Expr paramsPutExpr = + MethodInvocationExpr.builder() + .setExprReferenceExpr(serializerVarExpr) + .setMethodName(serializerMethodName) + .setArguments(paramsPutArgs.build()) + .setReturnType(extractorReturnType) + .build(); + bodyStatements.add(ExprStatement.withExpr(paramsPutExpr)); + } + + return LambdaExpr.builder() + .setArguments(requestVarExpr.toBuilder().setIsDecl(true).build()) + .setBody(bodyStatements) + .setReturnExpr(fieldsVarExpr) + .build(); + } + + static String getBindingFieldMethodName( + HttpBinding httpBindingField, int descendantFieldsLengths, int index, String currFieldName) { + if (index == descendantFieldsLengths - 1) { + if (httpBindingField.isRepeated()) { + return String.format("get%sList", currFieldName); + } + if (httpBindingField.isEnum()) { + return String.format("get%sValue", currFieldName); + } + } + return String.format("get%s", currFieldName); + } +} diff --git a/sdk-platform-java/gapic-generator-java/src/main/java/com/google/api/generator/gapic/composer/rest/HttpJsonServiceStubClassComposer.java b/sdk-platform-java/gapic-generator-java/src/main/java/com/google/api/generator/gapic/composer/rest/HttpJsonServiceStubClassComposer.java index f3ca00f9db0f..685f3784bc8d 100644 --- a/sdk-platform-java/gapic-generator-java/src/main/java/com/google/api/generator/gapic/composer/rest/HttpJsonServiceStubClassComposer.java +++ b/sdk-platform-java/gapic-generator-java/src/main/java/com/google/api/generator/gapic/composer/rest/HttpJsonServiceStubClassComposer.java @@ -17,7 +17,6 @@ import com.google.api.HttpRule; import com.google.api.core.InternalApi; import com.google.api.gax.httpjson.ApiMethodDescriptor; -import com.google.api.gax.httpjson.ApiMethodDescriptor.MethodType; import com.google.api.gax.httpjson.FieldsExtractor; import com.google.api.gax.httpjson.HttpJsonCallSettings; import com.google.api.gax.httpjson.HttpJsonLongRunningClient; @@ -30,13 +29,10 @@ import com.google.api.gax.longrunning.OperationSnapshot; import com.google.api.gax.rpc.LongRunningClient; import com.google.api.gax.rpc.UnaryCallable; -import com.google.api.generator.engine.ast.AnnotationNode; import com.google.api.generator.engine.ast.AssignmentExpr; import com.google.api.generator.engine.ast.ConcreteReference; -import com.google.api.generator.engine.ast.EnumRefExpr; import com.google.api.generator.engine.ast.Expr; import com.google.api.generator.engine.ast.ExprStatement; -import com.google.api.generator.engine.ast.IfStatement; import com.google.api.generator.engine.ast.LambdaExpr; import com.google.api.generator.engine.ast.MethodDefinition; import com.google.api.generator.engine.ast.MethodInvocationExpr; @@ -55,16 +51,12 @@ import com.google.api.generator.gapic.composer.common.AbstractTransportServiceStubClassComposer; import com.google.api.generator.gapic.composer.store.TypeStore; import com.google.api.generator.gapic.model.GapicContext; -import com.google.api.generator.gapic.model.HttpBindings.HttpBinding; import com.google.api.generator.gapic.model.Message; import com.google.api.generator.gapic.model.Method; import com.google.api.generator.gapic.model.OperationResponse; import com.google.api.generator.gapic.model.Service; import com.google.api.generator.gapic.utils.JavaStyle; -import com.google.common.annotations.VisibleForTesting; -import com.google.common.base.Strings; import com.google.common.collect.BiMap; -import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; import com.google.protobuf.TypeRegistry; import java.util.ArrayList; @@ -85,8 +77,8 @@ public class HttpJsonServiceStubClassComposer extends AbstractTransportServiceSt private static final HttpJsonServiceStubClassComposer INSTANCE = new HttpJsonServiceStubClassComposer(); - private static final TypeStore FIXED_REST_TYPESTORE = createStaticTypes(); - private static final VariableExpr TYPE_REGISTRY_VAR_EXPR = + static final TypeStore FIXED_REST_TYPESTORE = createStaticTypes(); + static final VariableExpr TYPE_REGISTRY_VAR_EXPR = VariableExpr.builder() .setVariable( Variable.builder() @@ -137,33 +129,11 @@ protected Statement createMethodDescriptorVariableDecl( Map messageTypes, boolean restNumericEnumsEnabled) { MethodInvocationExpr expr = - MethodInvocationExpr.builder() - .setMethodName("newBuilder") - .setStaticReferenceType( - FIXED_REST_TYPESTORE.get(ApiMethodDescriptor.class.getSimpleName())) - .setGenerics(methodDescriptorVarExpr.variable().type().reference().generics()) - .build(); + HttpJsonDescriptorComposer.createMethodDescriptorExpr( + service, protoMethod, methodDescriptorVarExpr, restNumericEnumsEnabled, null); BiFunction, Function> - methodMaker = getMethodMaker(); - - String codeMethodArgName = getProtoRpcFullMethodName(service, protoMethod); - expr = - methodMaker - .apply( - "setFullMethodName", - Arrays.asList(ValueExpr.withValue(StringObjectValue.withValue(codeMethodArgName)))) - .apply(expr); - - expr = methodMaker.apply("setHttpMethod", getHttpMethodTypeExpr(protoMethod)).apply(expr); - expr = methodMaker.apply("setType", getMethodTypeExpr(protoMethod)).apply(expr); - expr = - methodMaker - .apply( - "setRequestFormatter", - getRequestFormatterExpr(protoMethod, restNumericEnumsEnabled)) - .apply(expr); - expr = methodMaker.apply("setResponseParser", setResponseParserExpr(protoMethod)).apply(expr); + methodMaker = HttpJsonDescriptorComposer.getMethodMaker(); if (protoMethod.isOperationPollingMethod() || protoMethod.hasLro()) { expr = @@ -217,192 +187,14 @@ protected List createGetMethodDescriptorsMethod( Service service, TypeStore typeStore, Map protoMethodNameToDescriptorVarExprs) { - - List bodyExprs = new ArrayList<>(); - - VariableExpr methodDescriptorsVarExpr = - VariableExpr.withVariable( - Variable.builder() - .setType( - TypeNode.withReference( - ConcreteReference.builder() - .setClazz(List.class) - .setGenerics( - Arrays.asList( - FIXED_REST_TYPESTORE.get("ApiMethodDescriptor").reference())) - .build())) - .setName("methodDescriptors") - .build()); - - bodyExprs.add( - AssignmentExpr.builder() - .setVariableExpr(methodDescriptorsVarExpr.toBuilder().setIsDecl(true).build()) - .setValueExpr( - NewObjectExpr.builder() - .setType(FIXED_REST_TYPESTORE.get("ArrayList")) - .setIsGeneric(true) - .build()) - .build()); - - for (VariableExpr methodDescriptorVarExpr : protoMethodNameToDescriptorVarExprs.values()) { - bodyExprs.add( - MethodInvocationExpr.builder() - .setExprReferenceExpr(methodDescriptorsVarExpr) - .setMethodName("add") - .setArguments(methodDescriptorVarExpr) - .build()); - } - - return Arrays.asList( - MethodDefinition.builder() - .setScope(ScopeNode.PUBLIC) - .setIsStatic(true) - .setReturnType(methodDescriptorsVarExpr.type()) - .setReturnExpr(methodDescriptorsVarExpr) - .setAnnotations( - Arrays.asList(AnnotationNode.withType(FIXED_REST_TYPESTORE.get("InternalApi")))) - .setName("getMethodDescriptors") - .setBody(bodyExprs.stream().map(ExprStatement::withExpr).collect(Collectors.toList())) - .build()); + return Collections.singletonList( + HttpJsonDescriptorComposer.createGetMethodDescriptorsMethod( + protoMethodNameToDescriptorVarExprs.values())); } - private BiFunction, Function> + static BiFunction, Function> getMethodMaker() { - return (mName, argExpr) -> - (m) -> - MethodInvocationExpr.builder() - .setMethodName(mName) - .setArguments(argExpr) - .setExprReferenceExpr(m) - .build(); - } - - private List getRequestFormatterExpr(Method protoMethod, boolean restNumericEnumsEnabled) { - BiFunction, Function> - methodMaker = getMethodMaker(); - - MethodInvocationExpr expr = - MethodInvocationExpr.builder() - .setStaticReferenceType( - FIXED_REST_TYPESTORE.get(ProtoMessageRequestFormatter.class.getSimpleName())) - .setMethodName("newBuilder") - .setGenerics(Collections.singletonList(protoMethod.inputType().reference())) - .build(); - - TypeNode extractorVarType = - TypeNode.withReference( - ConcreteReference.builder() - .setClazz(Map.class) - .setGenerics(TypeNode.STRING.reference(), TypeNode.STRING.reference()) - .build()); - - expr = - methodMaker - .apply( - "setPath", - Arrays.asList( - ValueExpr.withValue( - StringObjectValue.withValue( - protoMethod.httpBindings().lowerCamelPattern())), - createFieldsExtractorClassInstance( - protoMethod, - extractorVarType, - protoMethod.httpBindings().pathParameters(), - "putPathParam", - restNumericEnumsEnabled))) - .apply(expr); - - if (!protoMethod.httpBindings().lowerCamelAdditionalPatterns().isEmpty()) { - expr = - methodMaker - .apply( - "setAdditionalPaths", - protoMethod.httpBindings().lowerCamelAdditionalPatterns().stream() - .map(a -> ValueExpr.withValue(StringObjectValue.withValue(a))) - .collect(Collectors.toList())) - .apply(expr); - } - TypeNode fieldsVarGenericType = - TypeNode.withReference( - ConcreteReference.builder() - .setClazz(List.class) - .setGenerics(TypeNode.STRING.reference()) - .build()); - - extractorVarType = - TypeNode.withReference( - ConcreteReference.builder() - .setClazz(Map.class) - .setGenerics(TypeNode.STRING.reference(), fieldsVarGenericType.reference()) - .build()); - - expr = - methodMaker - .apply( - "setQueryParamsExtractor", - Arrays.asList( - createFieldsExtractorClassInstance( - protoMethod, - extractorVarType, - protoMethod.httpBindings().queryParameters(), - "putQueryParam", - restNumericEnumsEnabled))) - .apply(expr); - - extractorVarType = TypeNode.STRING; - boolean asteriskBody = protoMethod.httpBindings().isAsteriskBody(); - expr = - methodMaker - .apply( - "setRequestBodyExtractor", - Arrays.asList( - createBodyFieldsExtractorClassInstance( - protoMethod, - extractorVarType, - asteriskBody - ? protoMethod.httpBindings().pathParameters() - : protoMethod.httpBindings().bodyParameters(), - "toBody", - asteriskBody, - restNumericEnumsEnabled))) - .apply(expr); - expr = methodMaker.apply("build", Collections.emptyList()).apply(expr); - - return Collections.singletonList(expr); - } - - private List setResponseParserExpr(Method protoMethod) { - BiFunction, Function> - methodMaker = getMethodMaker(); - - MethodInvocationExpr expr = - MethodInvocationExpr.builder() - .setStaticReferenceType( - FIXED_REST_TYPESTORE.get(ProtoMessageResponseParser.class.getSimpleName())) - .setMethodName("newBuilder") - .setGenerics(Collections.singletonList(protoMethod.outputType().reference())) - // .setArguments(Arrays.asList(m)) - .build(); - - expr = - methodMaker - .apply( - "setDefaultInstance", - Arrays.asList( - MethodInvocationExpr.builder() - .setStaticReferenceType(protoMethod.outputType()) - .setMethodName("getDefaultInstance") - .setReturnType(protoMethod.outputType()) - .build())) - .apply(expr); - - expr = - methodMaker - .apply("setDefaultTypeRegistry", Arrays.asList(TYPE_REGISTRY_VAR_EXPR)) - .apply(expr); - expr = methodMaker.apply("build", Collections.emptyList()).apply(expr); - - return Collections.singletonList(expr); + return HttpJsonDescriptorComposer.getMethodMaker(); } // Generates get[camelCase(fieldName)] @@ -726,320 +518,8 @@ private List setPollingRequestFactoryExpr( .build()); } - private Expr createBodyFieldsExtractorClassInstance( - Method method, - TypeNode extractorReturnType, - Set httpBindingFieldNames, - String serializerMethodName, - boolean asteriskBody, - boolean restNumericEnumEnabled) { - List bodyStatements = new ArrayList<>(); - - Expr returnExpr = null; - Expr serializerExpr = - MethodInvocationExpr.builder() - .setMethodName("create") - .setStaticReferenceType( - FIXED_REST_TYPESTORE.get(ProtoRestSerializer.class.getSimpleName())) - .build(); - - VariableExpr requestVarExpr = - VariableExpr.withVariable( - Variable.builder().setType(method.inputType()).setName("request").build()); - Expr bodyRequestExpr = requestVarExpr; - String requestMethodPrefix = "get"; - String bodyParamName = null; - - if (asteriskBody) { - bodyRequestExpr = - MethodInvocationExpr.builder() - .setExprReferenceExpr(requestVarExpr) - .setMethodName("toBuilder") - .build(); - // In case of `body: "*"` case we send the whole request message as a body, minus the fields - // in the path, therefore the "clear" prefix here. - requestMethodPrefix = "clear"; - } - - Expr prevExpr = bodyRequestExpr; - for (HttpBinding httpBindingFieldName : httpBindingFieldNames) { - // Handle foo.bar cases by descending into the subfields. - MethodInvocationExpr.Builder requestFieldMethodExprBuilder = - MethodInvocationExpr.builder().setExprReferenceExpr(prevExpr); - // Use explicit json_name if defined in the proto, prioritizing the actual wire name - // over Java-escaped identifiers. Note that trailing underscores (e.g., 'case_') result from: - // 1. protoc-gen-java: - // https://github.com/protocolbuffers/protobuf/blob/cecbbf41e43634c7c5b940dd336aa81b31fd4e5d/src/google/protobuf/compiler/java/names.cc#L189-L195 - // 2. gapic-generator-java Keyword implementation: - // com/google/api/generator/engine/lexicon/Keyword.java#L92-L94 - bodyParamName = - !Strings.isNullOrEmpty(httpBindingFieldName.jsonName()) - ? httpBindingFieldName.jsonName() - : JavaStyle.toLowerCamelCase(httpBindingFieldName.name()); - String[] descendantFields = httpBindingFieldName.name().split("\\."); - if (asteriskBody && descendantFields.length > 1) { - // This is the `body: "*"` case, do not clean nested body fields as it a very rare, not - // well-defined case, and it is generally safer to send more than less in such case. - continue; - } - - for (int i = 0; i < descendantFields.length; i++) { - String currFieldName = descendantFields[i]; - String bindingFieldMethodName = - String.format("%s%s", requestMethodPrefix, JavaStyle.toUpperCamelCase(currFieldName)); - requestFieldMethodExprBuilder = - requestFieldMethodExprBuilder.setMethodName(bindingFieldMethodName); - - if (i < descendantFields.length - 1) { - requestFieldMethodExprBuilder = - MethodInvocationExpr.builder() - .setExprReferenceExpr(requestFieldMethodExprBuilder.build()); - } - } - prevExpr = requestFieldMethodExprBuilder.build(); - } - - if (httpBindingFieldNames.isEmpty() && !asteriskBody) { - returnExpr = ValueExpr.createNullExpr(); - } else { - ImmutableList.Builder paramsPutArgs = ImmutableList.builder(); - if (asteriskBody) { - prevExpr = - MethodInvocationExpr.builder() - .setExprReferenceExpr(prevExpr) - .setMethodName("build") - .build(); - bodyParamName = "*"; - } - paramsPutArgs.add(ValueExpr.withValue(StringObjectValue.withValue(bodyParamName))); - paramsPutArgs.add(prevExpr); - - PrimitiveValue primitiveValue = - PrimitiveValue.builder() - .setType(TypeNode.BOOLEAN) - .setValue(String.valueOf(restNumericEnumEnabled)) - .build(); - paramsPutArgs.add(ValueExpr.withValue(primitiveValue)); - - returnExpr = - MethodInvocationExpr.builder() - .setExprReferenceExpr(serializerExpr) - .setMethodName(serializerMethodName) - .setArguments(paramsPutArgs.build()) - .setReturnType(extractorReturnType) - .build(); - } - - // Overrides FieldsExtractor - // (https://github.com/googleapis/gax-java/blob/12b18ee255d3fabe13bb3969df40753b29f830d5/gax-httpjson/src/main/java/com/google/api/gax/httpjson/FieldsExtractor.java). - return LambdaExpr.builder() - .setArguments(requestVarExpr.toBuilder().setIsDecl(true).build()) - .setBody(bodyStatements) - .setReturnExpr(returnExpr) - .build(); - } - - private Expr createFieldsExtractorClassInstance( - Method method, - TypeNode extractorReturnType, - Set httpBindingFieldNames, - String serializerMethodName, - boolean restNumericEnumsEnabled) { - List bodyStatements = new ArrayList<>(); - - VariableExpr fieldsVarExpr = - VariableExpr.withVariable( - Variable.builder().setName("fields").setType(extractorReturnType).build()); - Expr fieldsAssignExpr = - AssignmentExpr.builder() - .setVariableExpr(fieldsVarExpr.toBuilder().setIsDecl(true).build()) - .setValueExpr( - NewObjectExpr.builder() - .setType(FIXED_REST_TYPESTORE.get(HashMap.class.getSimpleName())) - .setIsGeneric(true) - .build()) - .build(); - - bodyStatements.add(ExprStatement.withExpr(fieldsAssignExpr)); - - TypeNode serializerVarType = - TypeNode.withReference( - ConcreteReference.builder() - .setClazz(ProtoRestSerializer.class) - .setGenerics(method.inputType().reference()) - .build()); - - VariableExpr serializerVarExpr = - VariableExpr.withVariable( - Variable.builder().setName("serializer").setType(serializerVarType).build()); - - Expr serializerAssignExpr = - AssignmentExpr.builder() - .setVariableExpr(serializerVarExpr.toBuilder().setIsDecl(true).build()) - .setValueExpr( - MethodInvocationExpr.builder() - .setStaticReferenceType( - FIXED_REST_TYPESTORE.get(ProtoRestSerializer.class.getSimpleName())) - .setMethodName("create") - .setReturnType(serializerVarType) - .build()) - .build(); - - bodyStatements.add(ExprStatement.withExpr(serializerAssignExpr)); - - VariableExpr requestVarExpr = - VariableExpr.withVariable( - Variable.builder().setType(method.inputType()).setName("request").build()); - - for (HttpBinding httpBindingFieldName : httpBindingFieldNames) { - // Handle foo.bar cases by descending into the subfields. - MethodInvocationExpr.Builder requestFieldGetterExprBuilder = - MethodInvocationExpr.builder().setExprReferenceExpr(requestVarExpr); - MethodInvocationExpr.Builder requestFieldHasExprBuilder = - MethodInvocationExpr.builder().setExprReferenceExpr(requestVarExpr); - String[] descendantFields = httpBindingFieldName.name().split("\\."); - for (int i = 0; i < descendantFields.length; i++) { - String currFieldName = descendantFields[i]; - String bindingFieldMethodName = - getBindingFieldMethodName( - httpBindingFieldName, - descendantFields.length, - i, - JavaStyle.toUpperCamelCase(currFieldName)); - requestFieldGetterExprBuilder = - requestFieldGetterExprBuilder.setMethodName(bindingFieldMethodName); - - String bindingFieldHasMethodName = - (i < descendantFields.length - 1) - ? bindingFieldMethodName - : String.format("has%s", JavaStyle.toUpperCamelCase(currFieldName)); - requestFieldHasExprBuilder = - requestFieldHasExprBuilder - .setMethodName(bindingFieldHasMethodName) - .setReturnType(TypeNode.BOOLEAN); - - if (i < descendantFields.length - 1) { - requestFieldGetterExprBuilder = - MethodInvocationExpr.builder() - .setExprReferenceExpr(requestFieldGetterExprBuilder.build()); - requestFieldHasExprBuilder = - MethodInvocationExpr.builder() - .setExprReferenceExpr(requestFieldHasExprBuilder.build()); - } - } - - MethodInvocationExpr requestBuilderExpr = requestFieldGetterExprBuilder.build(); - MethodInvocationExpr requestHasExpr = requestFieldHasExprBuilder.build(); - - ImmutableList.Builder paramsPutArgs = ImmutableList.builder(); - - paramsPutArgs.add(fieldsVarExpr); - - paramsPutArgs.add( - ValueExpr.withValue( - StringObjectValue.withValue( - // Use explicit json_name if defined in the proto, prioritizing the actual wire - // name over Java-escaped identifiers (e.g., avoiding 'case_' generated to prevent - // keywords conflict). - (httpBindingFieldName.jsonName() != null) - ? httpBindingFieldName.jsonName() - : JavaStyle.toLowerCamelCase(httpBindingFieldName.name())))); - paramsPutArgs.add(requestBuilderExpr); - - Expr paramsPutExpr = - MethodInvocationExpr.builder() - .setExprReferenceExpr(serializerVarExpr) - .setMethodName(serializerMethodName) - .setArguments(paramsPutArgs.build()) - .setReturnType(extractorReturnType) - .build(); - - if (httpBindingFieldName.isOptional()) { - bodyStatements.add( - IfStatement.builder() - .setConditionExpr(requestHasExpr) - .setBody(Arrays.asList(ExprStatement.withExpr(paramsPutExpr))) - .build()); - } else { - bodyStatements.add(ExprStatement.withExpr(paramsPutExpr)); - } - } - - // Add a fixed query param for numeric enum, see b/232457244 for details - if (restNumericEnumsEnabled && serializerMethodName.equals("putQueryParam")) { - ImmutableList.Builder paramsPutArgs = ImmutableList.builder(); - - paramsPutArgs.add(fieldsVarExpr); - paramsPutArgs.add(ValueExpr.withValue(StringObjectValue.withValue("$alt"))); - paramsPutArgs.add(ValueExpr.withValue(StringObjectValue.withValue("json;enum-encoding=int"))); - - Expr paramsPutExpr = - MethodInvocationExpr.builder() - .setExprReferenceExpr(serializerVarExpr) - .setMethodName(serializerMethodName) - .setArguments(paramsPutArgs.build()) - .setReturnType(extractorReturnType) - .build(); - bodyStatements.add(ExprStatement.withExpr(paramsPutExpr)); - } - - // Overrides FieldsExtractor - // (https://github.com/googleapis/gax-java/blob/12b18ee255d3fabe13bb3969df40753b29f830d5/gax-httpjson/src/main/java/com/google/api/gax/httpjson/FieldsExtractor.java). - return LambdaExpr.builder() - .setArguments(requestVarExpr.toBuilder().setIsDecl(true).build()) - .setBody(bodyStatements) - .setReturnExpr(fieldsVarExpr) - .build(); - } - - @VisibleForTesting - String getBindingFieldMethodName( - HttpBinding httpBindingField, int descendantFieldsLengths, int index, String currFieldName) { - if (index == descendantFieldsLengths - 1) { - if (httpBindingField.isRepeated()) { - return String.format("get%sList", currFieldName); - } - if (httpBindingField.isEnum()) { - return String.format("get%sValue", currFieldName); - } - } - return String.format("get%s", currFieldName); - } - - private List getHttpMethodTypeExpr(Method protoMethod) { - return Collections.singletonList( - ValueExpr.withValue( - StringObjectValue.withValue(protoMethod.httpBindings().httpVerb().toString()))); - } - - private List getMethodTypeExpr(Method protoMethod) { - MethodType methodType; - switch (protoMethod.stream()) { - case NONE: - methodType = MethodType.UNARY; - break; - case SERVER: - methodType = MethodType.SERVER_STREAMING; - break; - case CLIENT: - // Not feasible to suppor in REST - case BIDI: - // Not feasible to suppor in REST - default: - throw new UnsupportedOperationException( - String.format( - "Methods of type %s are not supported by REST transport", protoMethod.stream())); - } - EnumRefExpr expr = - EnumRefExpr.builder() - .setName(methodType.toString()) - .setType( - TypeNode.withReference( - ConcreteReference.builder() - .setClazz(ApiMethodDescriptor.MethodType.class) - .build())) - .build(); - return Collections.singletonList(expr); + static String getProtoRpcName(Service protoService, Method protoMethod) { + return HttpJsonDescriptorComposer.getProtoRpcFullMethodName(protoService, protoMethod); } @Override @@ -1365,7 +845,8 @@ private List createInvalidClassMethods(Service service) { .setType(FIXED_TYPESTORE.get("UnsupportedOperationException")) .setMessageExpr( String.format( - "Not implemented: %s(). %s transport is not implemented for this method yet.", + "Not implemented: %s(). %s transport is not implemented for" + + " this method yet.", callableName, getTransportContext().transport())) .build()))) .build()); diff --git a/sdk-platform-java/gapic-generator-java/src/test/java/com/google/api/generator/gapic/composer/rest/HttpJsonDescriptorComposerTest.java b/sdk-platform-java/gapic-generator-java/src/test/java/com/google/api/generator/gapic/composer/rest/HttpJsonDescriptorComposerTest.java new file mode 100644 index 000000000000..8037b50d4cbf --- /dev/null +++ b/sdk-platform-java/gapic-generator-java/src/test/java/com/google/api/generator/gapic/composer/rest/HttpJsonDescriptorComposerTest.java @@ -0,0 +1,212 @@ +// Copyright 2026 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package com.google.api.generator.gapic.composer.rest; + +import com.google.api.generator.engine.ast.Expr; +import com.google.api.generator.engine.ast.TypeNode; +import com.google.api.generator.engine.writer.JavaWriterVisitor; +import com.google.api.generator.gapic.model.Field; +import com.google.api.generator.gapic.model.GapicContext; +import com.google.api.generator.gapic.model.HttpBindings; +import com.google.api.generator.gapic.model.HttpBindings.HttpBinding; +import com.google.api.generator.gapic.model.Method; +import com.google.api.generator.gapic.model.Service; +import com.google.api.generator.test.protoloader.RestTestProtoLoader; +import static com.google.common.truth.Truth.assertThat; + +import java.util.Arrays; +import java.util.List; +import org.junit.jupiter.api.Test; + +class HttpJsonDescriptorComposerTest { + + private static final String RESUMABLE_UPLOAD_PREFIX = "/resumable/upload"; + private static final String TEST_PATH = "/v1/foo"; + + @Test + void prependPathPrefix_nullPrefix_returnsOriginalPath() { + assertThat(HttpJsonDescriptorComposer.prependPathPrefix(TEST_PATH, null)).isEqualTo(TEST_PATH); + assertThat(HttpJsonDescriptorComposer.prependPathPrefix("v1/foo", null)).isEqualTo("v1/foo"); + } + + @Test + void prependPathPrefix_barePrefix_normalizesSlashes() { + assertThat(HttpJsonDescriptorComposer.prependPathPrefix("v1/foo", "resumable/upload")) + .isEqualTo("/resumable/upload/v1/foo"); + assertThat(HttpJsonDescriptorComposer.prependPathPrefix(TEST_PATH, "resumable/upload")) + .isEqualTo("/resumable/upload/v1/foo"); + } + + @Test + void prependPathPrefix_slashWrappedPrefix_normalizesSlashes() { + assertThat(HttpJsonDescriptorComposer.prependPathPrefix("v1/foo", "/resumable/upload/")) + .isEqualTo("/resumable/upload/v1/foo"); + assertThat(HttpJsonDescriptorComposer.prependPathPrefix(TEST_PATH, "/resumable/upload/")) + .isEqualTo("/resumable/upload/v1/foo"); + } + + @Test + void prependPathPrefix_leadingSlashOnlyPrefix_normalizesSlashes() { + assertThat(HttpJsonDescriptorComposer.prependPathPrefix("v1/foo", RESUMABLE_UPLOAD_PREFIX)) + .isEqualTo("/resumable/upload/v1/foo"); + assertThat(HttpJsonDescriptorComposer.prependPathPrefix(TEST_PATH, RESUMABLE_UPLOAD_PREFIX)) + .isEqualTo("/resumable/upload/v1/foo"); + } + + @Test + void prependPathPrefix_trailingSlashOnlyPrefix_normalizesSlashes() { + assertThat(HttpJsonDescriptorComposer.prependPathPrefix("v1/foo", "resumable/upload/")) + .isEqualTo("/resumable/upload/v1/foo"); + assertThat(HttpJsonDescriptorComposer.prependPathPrefix(TEST_PATH, "resumable/upload/")) + .isEqualTo("/resumable/upload/v1/foo"); + } + + @Test + void getRequestFormatterExpr_withPathPrefix_prependsPrefix() { + GapicContext context = RestTestProtoLoader.instance().parseShowcaseResumableUpload(); + Service service = context.services().get(0); + Method uploadMethod = + service.methods().stream().filter(Method::isResumableUpload).findFirst().get(); + + List exprs = + HttpJsonDescriptorComposer.getRequestFormatterExpr( + uploadMethod, context.restNumericEnumsEnabled(), RESUMABLE_UPLOAD_PREFIX); + + JavaWriterVisitor visitor = new JavaWriterVisitor(); + for (Expr expr : exprs) { + expr.accept(visitor); + } + String code = visitor.write(); + assertThat(code).contains("/resumable/upload/v1beta1/files:upload"); + } + + @Test + void getRequestFormatterExpr_withAdditionalPaths_prependsPrefix() { + GapicContext context = RestTestProtoLoader.instance().parseShowcaseResumableUpload(); + Service service = context.services().get(0); + Method uploadMethod = + service.methods().stream().filter(Method::isResumableUpload).findFirst().get(); + + HttpBindings bindingsWithAdditional = + HttpBindings.builder() + .setHttpVerb(uploadMethod.httpBindings().httpVerb()) + .setPattern(uploadMethod.httpBindings().pattern()) + .setAdditionalPatterns(Arrays.asList("/v1beta1/files:additionalUpload")) + .setPathParameters(uploadMethod.httpBindings().pathParameters()) + .setQueryParameters(uploadMethod.httpBindings().queryParameters()) + .setBodyParameters(uploadMethod.httpBindings().bodyParameters()) + .setIsAsteriskBody(uploadMethod.httpBindings().isAsteriskBody()) + .build(); + Method methodWithAdditional = + uploadMethod.toBuilder().setHttpBindings(bindingsWithAdditional).build(); + + List exprs = + HttpJsonDescriptorComposer.getRequestFormatterExpr( + methodWithAdditional, context.restNumericEnumsEnabled(), RESUMABLE_UPLOAD_PREFIX); + + JavaWriterVisitor visitor = new JavaWriterVisitor(); + for (Expr expr : exprs) { + expr.accept(visitor); + } + String code = visitor.write(); + assertThat(code).contains("/resumable/upload/v1beta1/files:upload"); + assertThat(code).contains("setAdditionalPaths"); + assertThat(code).contains("/resumable/upload/v1beta1/files:additionalUpload"); + } + + @Test + void getHttpMethodTypeExpr_returnsCorrectVerb() { + GapicContext context = RestTestProtoLoader.instance().parseShowcaseResumableUpload(); + Service service = context.services().get(0); + Method uploadMethod = + service.methods().stream().filter(Method::isResumableUpload).findFirst().get(); + + List exprs = HttpJsonDescriptorComposer.getHttpMethodTypeExpr(uploadMethod); + JavaWriterVisitor visitor = new JavaWriterVisitor(); + for (Expr expr : exprs) { + expr.accept(visitor); + } + assertThat(visitor.write()).contains("\"POST\""); + } + + @Test + void getMethodTypeExpr_unaryAndServerStreaming() { + GapicContext context = RestTestProtoLoader.instance().parseEcho(); + Service service = context.services().get(0); + Method echoMethod = + service.methods().stream().filter(m -> m.name().equals("Echo")).findFirst().get(); + Method expandMethod = + service.methods().stream().filter(m -> m.name().equals("Expand")).findFirst().get(); + + List unaryExprs = HttpJsonDescriptorComposer.getMethodTypeExpr(echoMethod); + JavaWriterVisitor unaryVisitor = new JavaWriterVisitor(); + for (Expr expr : unaryExprs) { + expr.accept(unaryVisitor); + } + assertThat(unaryVisitor.write()).contains("MethodType.UNARY"); + + List streamExprs = HttpJsonDescriptorComposer.getMethodTypeExpr(expandMethod); + JavaWriterVisitor streamVisitor = new JavaWriterVisitor(); + for (Expr expr : streamExprs) { + expr.accept(streamVisitor); + } + assertThat(streamVisitor.write()).contains("MethodType.SERVER_STREAMING"); + } + + @Test + void setResponseParserExpr_containsDefaultInstanceAndRegistry() { + GapicContext context = RestTestProtoLoader.instance().parseShowcaseResumableUpload(); + Service service = context.services().get(0); + Method uploadMethod = + service.methods().stream().filter(Method::isResumableUpload).findFirst().get(); + + List exprs = HttpJsonDescriptorComposer.setResponseParserExpr(uploadMethod); + JavaWriterVisitor visitor = new JavaWriterVisitor(); + for (Expr expr : exprs) { + expr.accept(visitor); + } + String code = visitor.write(); + assertThat(code).contains("getDefaultInstance()"); + assertThat(code).contains("setDefaultTypeRegistry(typeRegistry)"); + } + + @Test + void getBindingFieldMethodName_allCases() { + Field repeatedField = + Field.builder().setName("values").setIsRepeated(true).setType(TypeNode.STRING).build(); + HttpBinding repeatedBinding = + HttpBinding.builder().setField(repeatedField).setName("values").build(); + assertThat( + HttpJsonDescriptorComposer.getBindingFieldMethodName(repeatedBinding, 1, 0, "Values")) + .isEqualTo("getValuesList"); + + Field enumField = + Field.builder().setName("enumVal").setIsEnum(true).setType(TypeNode.OBJECT).build(); + HttpBinding enumBinding = HttpBinding.builder().setField(enumField).setName("enumVal").build(); + assertThat(HttpJsonDescriptorComposer.getBindingFieldMethodName(enumBinding, 1, 0, "EnumVal")) + .isEqualTo("getEnumValValue"); + + Field regularField = Field.builder().setName("regularVal").setType(TypeNode.STRING).build(); + HttpBinding regularBinding = + HttpBinding.builder().setField(regularField).setName("regularVal").build(); + assertThat( + HttpJsonDescriptorComposer.getBindingFieldMethodName(regularBinding, 1, 0, "RegularVal")) + .isEqualTo("getRegularVal"); + + assertThat( + HttpJsonDescriptorComposer.getBindingFieldMethodName(regularBinding, 3, 1, "RegularVal")) + .isEqualTo("getRegularVal"); + } +} diff --git a/sdk-platform-java/gapic-generator-java/src/test/java/com/google/api/generator/gapic/composer/rest/HttpJsonServiceStubClassComposerTest.java b/sdk-platform-java/gapic-generator-java/src/test/java/com/google/api/generator/gapic/composer/rest/HttpJsonServiceStubClassComposerTest.java index 0e78c76c3646..25ee5da79ed0 100644 --- a/sdk-platform-java/gapic-generator-java/src/test/java/com/google/api/generator/gapic/composer/rest/HttpJsonServiceStubClassComposerTest.java +++ b/sdk-platform-java/gapic-generator-java/src/test/java/com/google/api/generator/gapic/composer/rest/HttpJsonServiceStubClassComposerTest.java @@ -19,12 +19,9 @@ import com.google.api.CustomHttpPattern; import com.google.api.Http; import com.google.api.HttpRule; -import com.google.api.generator.engine.ast.TypeNode; import com.google.api.generator.engine.writer.JavaWriterVisitor; -import com.google.api.generator.gapic.model.Field; import com.google.api.generator.gapic.model.GapicClass; import com.google.api.generator.gapic.model.GapicContext; -import com.google.api.generator.gapic.model.HttpBindings.HttpBinding; import com.google.api.generator.gapic.model.Service; import com.google.api.generator.test.framework.Assert; import com.google.api.generator.test.framework.GoldenFileWriter; @@ -76,48 +73,6 @@ void generateEchoServiceClasses() { Assert.assertCodeEquals(goldenFilePath, visitor.write()); } - @Test - void getBindingFieldMethodName_shouldReturnGetFieldListIfTheFieldIsInLastPositionAndIsRepeated() { - Field field = - Field.builder() - .setIsRepeated(true) - .setName("doesNotMatter") - .setType(TypeNode.OBJECT) - .build(); - HttpBinding httpBinding = - HttpBinding.builder().setField(field).setName("doesNotMatter").build(); - String actual = composer.getBindingFieldMethodName(httpBinding, 4, 3, "Values"); - Truth.assertThat(actual).isEqualTo("getValuesList"); - } - - @Test - void getBindingFieldMethodName_shouldReturnGetFieldValueIfTheFieldIsInLastPositionAndIsEnum() { - Field field = - Field.builder().setIsEnum(true).setName("doesNotMatter").setType(TypeNode.OBJECT).build(); - HttpBinding httpBinding = - HttpBinding.builder().setField(field).setName("doesNotMatter").build(); - String actual = composer.getBindingFieldMethodName(httpBinding, 4, 3, "Enums"); - Truth.assertThat(actual).isEqualTo("getEnumsValue"); - } - - @Test - void - getBindingFieldMethodName_shouldReturnGetFieldIfTheFieldIsInLastPositionAndNotRepeatedOrEnum() { - Field field = Field.builder().setName("doesNotMatter").setType(TypeNode.OBJECT).build(); - HttpBinding httpBinding = - HttpBinding.builder().setField(field).setName("doesNotMatter").build(); - String actual = composer.getBindingFieldMethodName(httpBinding, 4, 3, "Value"); - Truth.assertThat(actual).isEqualTo("getValue"); - } - - @Test - void getBindingFieldMethodName_shouldReturnGetFieldIfTheFieldIsNotInLastPosition() { - Field field = Field.builder().setName("doesNotMatter").setType(TypeNode.OBJECT).build(); - HttpBinding httpBinding = - HttpBinding.builder().setField(field).setName("doesNotMatter").build(); - String actual = composer.getBindingFieldMethodName(httpBinding, 4, 1, "Value"); - Truth.assertThat(actual).isEqualTo("getValue"); - } @Test void parseOperationsCustomHttpRules_shouldReturnMapIfContextContainsValidServiceYaml() { @@ -224,3 +179,4 @@ void generateHttpJsonServiceStubClass_clientResourceNameExtractor() { Assert.assertEmptySamples(clazz.samples()); } } +