Skip to content

Commit

Permalink
feat: Add google.api.routing annotation support (WIP)
Browse files Browse the repository at this point in the history
This is an implementation of the go/actools-dynamic-routing-proposal
which provides a way in an API description to specify a limited per-RPC
projection from RPC’s input message to a routing header.

NOTE: This is work-in-progress and not meant for review yet.
  • Loading branch information
meltsufin committed Dec 11, 2021
1 parent 6af0e18 commit 702ada4
Show file tree
Hide file tree
Showing 14 changed files with 188 additions and 37 deletions.
4 changes: 2 additions & 2 deletions repositories.bzl
Original file line number Diff line number Diff line change
Expand Up @@ -59,9 +59,9 @@ def gapic_generator_java_repositories():
_maybe(
http_archive,
name = "com_google_googleapis",
strip_prefix = "googleapis-ba30d8097582039ac4cc4e21b4e4baa426423075",
strip_prefix = "googleapis-987192dfddeb79d3262b9f9f7dbf092827f931ac",
urls = [
"https://github.com/googleapis/googleapis/archive/ba30d8097582039ac4cc4e21b4e4baa426423075.zip",
"https://github.com/googleapis/googleapis/archive/987192dfddeb79d3262b9f9f7dbf092827f931ac.zip",
],
)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -307,7 +307,11 @@ private LambdaExpr createRequestParamsExtractorClassInstance(Method method) {
.setExprReferenceExpr(paramsVarExpr)
.setMethodName("put")
.setArguments(
ValueExpr.withValue(StringObjectValue.withValue(httpBindingFieldBinding.name())),
ValueExpr.withValue(
StringObjectValue.withValue(
httpBindingFieldBinding.alias() != null
? httpBindingFieldBinding.alias()
: httpBindingFieldBinding.name())),
valueOfExpr)
.build();
bodyExprs.add(paramsPutExpr);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,8 +35,10 @@ public abstract static class HttpBinding implements Comparable<HttpBinding> {

public abstract boolean isOptional();

public static HttpBinding create(String name, boolean isOptional) {
return new AutoValue_HttpBindings_HttpBinding(name, isOptional);
public abstract String alias();

public static HttpBinding create(String name, boolean isOptional, String alias) {
return new AutoValue_HttpBindings_HttpBinding(name, isOptional, alias);
}

// Do not forget to keep it in sync with equals() implementation.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ java_library(
"//src/main/java/com/google/api/generator/gapic/model",
"//src/main/java/com/google/api/generator/gapic/utils",
"@com_google_api_api_common//jar",
"@com_google_api_grpc_proto_google_common_protos",
"@com_google_code_findbugs_jsr305//jar",
"@com_google_code_gson//jar",
"@com_google_googleapis//google/api:api_java_proto",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,9 @@
import com.google.api.AnnotationsProto;
import com.google.api.HttpRule;
import com.google.api.HttpRule.PatternCase;
import com.google.api.RoutingParameter;
import com.google.api.RoutingProto;
import com.google.api.RoutingRule;
import com.google.api.generator.gapic.model.Field;
import com.google.api.generator.gapic.model.HttpBindings;
import com.google.api.generator.gapic.model.HttpBindings.HttpBinding;
Expand All @@ -30,6 +33,7 @@
import com.google.protobuf.DescriptorProtos.MethodOptions;
import com.google.protobuf.Descriptors.MethodDescriptor;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
Expand All @@ -47,20 +51,28 @@ public static HttpBindings parse(

HttpRule httpRule = methodOptions.getExtension(AnnotationsProto.http);

RoutingRule routingRule = null;
if (methodOptions.hasExtension(RoutingProto.routing)) {
routingRule = methodOptions.getExtension(RoutingProto.routing);
}

// Body validation.
if (!Strings.isNullOrEmpty(httpRule.getBody()) && !httpRule.getBody().equals(ASTERISK)) {
checkHttpFieldIsValid(httpRule.getBody(), inputMessage, true);
}

return parseHttpRuleHelper(httpRule, Optional.of(inputMessage), messageTypes);
return parseHttpRuleHelper(httpRule, routingRule, Optional.of(inputMessage), messageTypes);
}

public static HttpBindings parseHttpRule(HttpRule httpRule) {
return parseHttpRuleHelper(httpRule, Optional.empty(), Collections.emptyMap());
return parseHttpRuleHelper(httpRule, null, Optional.empty(), Collections.emptyMap());
}

private static HttpBindings parseHttpRuleHelper(
HttpRule httpRule, Optional<Message> inputMessageOpt, Map<String, Message> messageTypes) {
HttpRule httpRule,
RoutingRule routingRule,
Optional<Message> inputMessageOpt,
Map<String, Message> messageTypes) {
// Get pattern.
String pattern = getHttpVerbPattern(httpRule);
ImmutableSet.Builder<String> bindingsBuilder = getPatternBindings(pattern);
Expand Down Expand Up @@ -94,31 +106,50 @@ private static HttpBindings parseHttpRuleHelper(
Sets.difference(inputMessageOpt.get().fieldMap().keySet(), bodyBinidngsUnion);
}

Map<String, String> fieldToAliasMap = null;
if (routingRule != null) {
fieldToAliasMap = new HashMap<>();
for (RoutingParameter routingParameter : routingRule.getRoutingParametersList()) {
Set<String> params = getPatternBindings(routingParameter.getPathTemplate()).build();
if (params.size() == 1) {
fieldToAliasMap.put(routingParameter.getField(), params.iterator().next());
}
}
}

Message message = inputMessageOpt.orElse(null);
return HttpBindings.builder()
.setHttpVerb(HttpBindings.HttpVerb.valueOf(httpRule.getPatternCase().toString()))
.setPattern(pattern)
.setPathParameters(
validateAndConstructHttpBindings(pathParamNames, message, messageTypes, true))
validateAndConstructHttpBindings(
pathParamNames, fieldToAliasMap, message, messageTypes, true))
.setQueryParameters(
validateAndConstructHttpBindings(queryParamNames, message, messageTypes, false))
validateAndConstructHttpBindings(queryParamNames, null, message, messageTypes, false))
.setBodyParameters(
validateAndConstructHttpBindings(bodyParamNames, message, messageTypes, false))
validateAndConstructHttpBindings(bodyParamNames, null, message, messageTypes, false))
.setIsAsteriskBody(body.equals(ASTERISK))
.build();
}

private static Set<HttpBinding> validateAndConstructHttpBindings(
Set<String> paramNames,
Map<String, String> fieldToAliasMap,
Message inputMessage,
Map<String, Message> messageTypes,
boolean isPath) {

if (fieldToAliasMap != null) {
paramNames = fieldToAliasMap.keySet();
}

ImmutableSortedSet.Builder<HttpBinding> httpBindings = ImmutableSortedSet.naturalOrder();
for (String paramName : paramNames) {
// Handle foo.bar cases by descending into the subfields.
String[] subFields = paramName.split("\\.");
if (inputMessage == null) {
httpBindings.add(HttpBinding.create(paramName, false));
httpBindings.add(
HttpBinding.create(paramName, false, getAlias(paramName, fieldToAliasMap)));
continue;
}
Message nestedMessage = inputMessage;
Expand All @@ -138,13 +169,23 @@ private static Set<HttpBinding> validateAndConstructHttpBindings(
checkHttpFieldIsValid(subFieldName, nestedMessage, !isPath);
}
Field field = nestedMessage.fieldMap().get(subFieldName);
httpBindings.add(HttpBinding.create(paramName, field.isProto3Optional()));
httpBindings.add(
HttpBinding.create(
paramName, field.isProto3Optional(), getAlias(paramName, fieldToAliasMap)));
}
}
}
return httpBindings.build();
}

private static String getAlias(String paramName, Map<String, String> fieldToAliasMap) {
if (fieldToAliasMap != null && fieldToAliasMap.containsKey(paramName)) {
return fieldToAliasMap.get(paramName);
} else {
return paramName;
}
}

private static String getHttpVerbPattern(HttpRule httpRule) {
PatternCase patternCase = httpRule.getPatternCase();
switch (patternCase) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -221,7 +221,7 @@ public class GrpcTestingStub extends TestingStub {
.setParamsExtractor(
request -> {
ImmutableMap.Builder<String, String> params = ImmutableMap.builder();
params.put("name", String.valueOf(request.getName()));
params.put("rename", String.valueOf(request.getName()));
return params.build();
})
.build();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -91,4 +91,23 @@ public void parseHttpAnnotation_missingFieldFromMessage() {
assertThrows(
IllegalStateException.class, () -> HttpRuleParser.parse(rpcMethod, inputMessage, messages));
}

@Test
public void parseRoutingAnnotation_alias() {
FileDescriptor testingFileDescriptor = TestingOuterClass.getDescriptor();
ServiceDescriptor testingService = testingFileDescriptor.getServices().get(0);
assertEquals("Testing", testingService.getName());

Map<String, Message> messages = Parser.parseMessages(testingFileDescriptor);

// GetTest method.
MethodDescriptor rpcMethod = testingService.getMethods().get(5);
Message inputMessage = messages.get("com.google.showcase.v1beta1.GetTestRequest");
HttpBindings httpBindings = HttpRuleParser.parse(rpcMethod, inputMessage, messages);
assertThat(
httpBindings.pathParameters().stream()
.map(binding -> binding.name() + " -> " + binding.alias())
.collect(Collectors.toList()))
.containsExactly("name -> rename");
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ proto_library(
],
deps = [
"@com_google_googleapis//google/api:annotations_proto",
"@com_google_googleapis//google/api:routing_proto",
"@com_google_googleapis//google/api:client_proto",
"@com_google_googleapis//google/api:field_behavior_proto",
"@com_google_googleapis//google/api:resource_proto",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
syntax = "proto3";

import "google/api/annotations.proto";
import "google/api/routing.proto";
import "google/api/client.proto";
import "google/api/resource.proto";
import "google/protobuf/empty.proto";
Expand Down Expand Up @@ -78,6 +79,12 @@ service Testing {
option (google.api.http) = {
get: "/v1beta1/{name=tests/*}"
};
option (google.api.routing) = {
routing_parameters {
field: "name"
path_template: "/v1beta1/{rename=tests/*}"
}
};
option (google.api.method_signature) = "name";
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@
* `/projects/{project_id}/locations/{location_id}/instances/{instance_id}`
* </ul>
*
* <p>Note that location_id must be refering to a GCP `region`; for example:
* <p>Note that location_id must be referring to a GCP `region`; for example:
*
* <ul>
* <li>`projects/redpepper-1290/locations/us-central1/instances/my-redis`
Expand Down Expand Up @@ -472,7 +472,7 @@ public final UnaryCallable<GetInstanceRequest, Instance> getInstanceCallable() {
*
* <p>The creation is executed asynchronously and callers may check the returned operation to
* track its progress. Once the operation is completed the Redis instance will be fully
* functional. Completed longrunning.Operation will contain the new instance object in the
* functional. The completed longrunning.Operation will contain the new instance object in the
* response field.
*
* <p>The returned operation is automatically deleted after a few hours, so there is no need to
Expand Down Expand Up @@ -524,7 +524,7 @@ public final OperationFuture<Instance, Any> createInstanceAsync(
*
* <p>The creation is executed asynchronously and callers may check the returned operation to
* track its progress. Once the operation is completed the Redis instance will be fully
* functional. Completed longrunning.Operation will contain the new instance object in the
* functional. The completed longrunning.Operation will contain the new instance object in the
* response field.
*
* <p>The returned operation is automatically deleted after a few hours, so there is no need to
Expand Down Expand Up @@ -576,7 +576,7 @@ public final OperationFuture<Instance, Any> createInstanceAsync(
*
* <p>The creation is executed asynchronously and callers may check the returned operation to
* track its progress. Once the operation is completed the Redis instance will be fully
* functional. Completed longrunning.Operation will contain the new instance object in the
* functional. The completed longrunning.Operation will contain the new instance object in the
* response field.
*
* <p>The returned operation is automatically deleted after a few hours, so there is no need to
Expand Down Expand Up @@ -612,7 +612,7 @@ public final OperationFuture<Instance, Any> createInstanceAsync(CreateInstanceRe
*
* <p>The creation is executed asynchronously and callers may check the returned operation to
* track its progress. Once the operation is completed the Redis instance will be fully
* functional. Completed longrunning.Operation will contain the new instance object in the
* functional. The completed longrunning.Operation will contain the new instance object in the
* response field.
*
* <p>The returned operation is automatically deleted after a few hours, so there is no need to
Expand Down Expand Up @@ -649,7 +649,7 @@ public final OperationFuture<Instance, Any> createInstanceAsync(CreateInstanceRe
*
* <p>The creation is executed asynchronously and callers may check the returned operation to
* track its progress. Once the operation is completed the Redis instance will be fully
* functional. Completed longrunning.Operation will contain the new instance object in the
* functional. The completed longrunning.Operation will contain the new instance object in the
* response field.
*
* <p>The returned operation is automatically deleted after a few hours, so there is no need to
Expand Down Expand Up @@ -696,7 +696,8 @@ public final UnaryCallable<CreateInstanceRequest, Operation> createInstanceCalla
* @param updateMask Required. Mask of fields to update. At least one path must be supplied in
* this field. The elements of the repeated paths field may only include these fields from
* [Instance][google.cloud.redis.v1beta1.Instance]:
* <p>&#42; `displayName` &#42; `labels` &#42; `memorySizeGb` &#42; `redisConfig`
* <p>&#42; `displayName` &#42; `labels` &#42; `memorySizeGb` &#42; `redisConfig` &#42;
* `replica_count`
* @param instance Required. Update description. Only fields specified in update_mask are updated.
* @throws com.google.api.gax.rpc.ApiException if the remote call fails
*/
Expand Down Expand Up @@ -1164,7 +1165,7 @@ public final UnaryCallable<ExportInstanceRequest, Operation> exportInstanceCalla

// AUTO-GENERATED DOCUMENTATION AND METHOD.
/**
* Initiates a failover of the master node to current replica node for a specific STANDARD tier
* Initiates a failover of the primary node to current replica node for a specific STANDARD tier
* Cloud Memorystore for Redis instance.
*
* <p>Sample code:
Expand Down Expand Up @@ -1197,7 +1198,7 @@ public final OperationFuture<Instance, Any> failoverInstanceAsync(

// AUTO-GENERATED DOCUMENTATION AND METHOD.
/**
* Initiates a failover of the master node to current replica node for a specific STANDARD tier
* Initiates a failover of the primary node to current replica node for a specific STANDARD tier
* Cloud Memorystore for Redis instance.
*
* <p>Sample code:
Expand Down Expand Up @@ -1230,7 +1231,7 @@ public final OperationFuture<Instance, Any> failoverInstanceAsync(

// AUTO-GENERATED DOCUMENTATION AND METHOD.
/**
* Initiates a failover of the master node to current replica node for a specific STANDARD tier
* Initiates a failover of the primary node to current replica node for a specific STANDARD tier
* Cloud Memorystore for Redis instance.
*
* <p>Sample code:
Expand All @@ -1255,7 +1256,7 @@ public final OperationFuture<Instance, Any> failoverInstanceAsync(

// AUTO-GENERATED DOCUMENTATION AND METHOD.
/**
* Initiates a failover of the master node to current replica node for a specific STANDARD tier
* Initiates a failover of the primary node to current replica node for a specific STANDARD tier
* Cloud Memorystore for Redis instance.
*
* <p>Sample code:
Expand All @@ -1280,7 +1281,7 @@ public final OperationFuture<Instance, Any> failoverInstanceAsync(

// AUTO-GENERATED DOCUMENTATION AND METHOD.
/**
* Initiates a failover of the master node to current replica node for a specific STANDARD tier
* Initiates a failover of the primary node to current replica node for a specific STANDARD tier
* Cloud Memorystore for Redis instance.
*
* <p>Sample code:
Expand Down
Loading

0 comments on commit 702ada4

Please sign in to comment.