Skip to content

Commit 702ada4

Browse files
committed
feat: Add google.api.routing annotation support (WIP)
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.
1 parent 6af0e18 commit 702ada4

File tree

14 files changed

+188
-37
lines changed

14 files changed

+188
-37
lines changed

repositories.bzl

+2-2
Original file line numberDiff line numberDiff line change
@@ -59,9 +59,9 @@ def gapic_generator_java_repositories():
5959
_maybe(
6060
http_archive,
6161
name = "com_google_googleapis",
62-
strip_prefix = "googleapis-ba30d8097582039ac4cc4e21b4e4baa426423075",
62+
strip_prefix = "googleapis-987192dfddeb79d3262b9f9f7dbf092827f931ac",
6363
urls = [
64-
"https://github.com/googleapis/googleapis/archive/ba30d8097582039ac4cc4e21b4e4baa426423075.zip",
64+
"https://github.com/googleapis/googleapis/archive/987192dfddeb79d3262b9f9f7dbf092827f931ac.zip",
6565
],
6666
)
6767

src/main/java/com/google/api/generator/gapic/composer/grpc/GrpcServiceStubClassComposer.java

+5-1
Original file line numberDiff line numberDiff line change
@@ -307,7 +307,11 @@ private LambdaExpr createRequestParamsExtractorClassInstance(Method method) {
307307
.setExprReferenceExpr(paramsVarExpr)
308308
.setMethodName("put")
309309
.setArguments(
310-
ValueExpr.withValue(StringObjectValue.withValue(httpBindingFieldBinding.name())),
310+
ValueExpr.withValue(
311+
StringObjectValue.withValue(
312+
httpBindingFieldBinding.alias() != null
313+
? httpBindingFieldBinding.alias()
314+
: httpBindingFieldBinding.name())),
311315
valueOfExpr)
312316
.build();
313317
bodyExprs.add(paramsPutExpr);

src/main/java/com/google/api/generator/gapic/model/HttpBindings.java

+4-2
Original file line numberDiff line numberDiff line change
@@ -35,8 +35,10 @@ public abstract static class HttpBinding implements Comparable<HttpBinding> {
3535

3636
public abstract boolean isOptional();
3737

38-
public static HttpBinding create(String name, boolean isOptional) {
39-
return new AutoValue_HttpBindings_HttpBinding(name, isOptional);
38+
public abstract String alias();
39+
40+
public static HttpBinding create(String name, boolean isOptional, String alias) {
41+
return new AutoValue_HttpBindings_HttpBinding(name, isOptional, alias);
4042
}
4143

4244
// Do not forget to keep it in sync with equals() implementation.

src/main/java/com/google/api/generator/gapic/protoparser/BUILD.bazel

+1
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ java_library(
2727
"//src/main/java/com/google/api/generator/gapic/model",
2828
"//src/main/java/com/google/api/generator/gapic/utils",
2929
"@com_google_api_api_common//jar",
30+
"@com_google_api_grpc_proto_google_common_protos",
3031
"@com_google_code_findbugs_jsr305//jar",
3132
"@com_google_code_gson//jar",
3233
"@com_google_googleapis//google/api:api_java_proto",

src/main/java/com/google/api/generator/gapic/protoparser/HttpRuleParser.java

+49-8
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,9 @@
1717
import com.google.api.AnnotationsProto;
1818
import com.google.api.HttpRule;
1919
import com.google.api.HttpRule.PatternCase;
20+
import com.google.api.RoutingParameter;
21+
import com.google.api.RoutingProto;
22+
import com.google.api.RoutingRule;
2023
import com.google.api.generator.gapic.model.Field;
2124
import com.google.api.generator.gapic.model.HttpBindings;
2225
import com.google.api.generator.gapic.model.HttpBindings.HttpBinding;
@@ -30,6 +33,7 @@
3033
import com.google.protobuf.DescriptorProtos.MethodOptions;
3134
import com.google.protobuf.Descriptors.MethodDescriptor;
3235
import java.util.Collections;
36+
import java.util.HashMap;
3337
import java.util.Map;
3438
import java.util.Optional;
3539
import java.util.Set;
@@ -47,20 +51,28 @@ public static HttpBindings parse(
4751

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

54+
RoutingRule routingRule = null;
55+
if (methodOptions.hasExtension(RoutingProto.routing)) {
56+
routingRule = methodOptions.getExtension(RoutingProto.routing);
57+
}
58+
5059
// Body validation.
5160
if (!Strings.isNullOrEmpty(httpRule.getBody()) && !httpRule.getBody().equals(ASTERISK)) {
5261
checkHttpFieldIsValid(httpRule.getBody(), inputMessage, true);
5362
}
5463

55-
return parseHttpRuleHelper(httpRule, Optional.of(inputMessage), messageTypes);
64+
return parseHttpRuleHelper(httpRule, routingRule, Optional.of(inputMessage), messageTypes);
5665
}
5766

5867
public static HttpBindings parseHttpRule(HttpRule httpRule) {
59-
return parseHttpRuleHelper(httpRule, Optional.empty(), Collections.emptyMap());
68+
return parseHttpRuleHelper(httpRule, null, Optional.empty(), Collections.emptyMap());
6069
}
6170

6271
private static HttpBindings parseHttpRuleHelper(
63-
HttpRule httpRule, Optional<Message> inputMessageOpt, Map<String, Message> messageTypes) {
72+
HttpRule httpRule,
73+
RoutingRule routingRule,
74+
Optional<Message> inputMessageOpt,
75+
Map<String, Message> messageTypes) {
6476
// Get pattern.
6577
String pattern = getHttpVerbPattern(httpRule);
6678
ImmutableSet.Builder<String> bindingsBuilder = getPatternBindings(pattern);
@@ -94,31 +106,50 @@ private static HttpBindings parseHttpRuleHelper(
94106
Sets.difference(inputMessageOpt.get().fieldMap().keySet(), bodyBinidngsUnion);
95107
}
96108

109+
Map<String, String> fieldToAliasMap = null;
110+
if (routingRule != null) {
111+
fieldToAliasMap = new HashMap<>();
112+
for (RoutingParameter routingParameter : routingRule.getRoutingParametersList()) {
113+
Set<String> params = getPatternBindings(routingParameter.getPathTemplate()).build();
114+
if (params.size() == 1) {
115+
fieldToAliasMap.put(routingParameter.getField(), params.iterator().next());
116+
}
117+
}
118+
}
119+
97120
Message message = inputMessageOpt.orElse(null);
98121
return HttpBindings.builder()
99122
.setHttpVerb(HttpBindings.HttpVerb.valueOf(httpRule.getPatternCase().toString()))
100123
.setPattern(pattern)
101124
.setPathParameters(
102-
validateAndConstructHttpBindings(pathParamNames, message, messageTypes, true))
125+
validateAndConstructHttpBindings(
126+
pathParamNames, fieldToAliasMap, message, messageTypes, true))
103127
.setQueryParameters(
104-
validateAndConstructHttpBindings(queryParamNames, message, messageTypes, false))
128+
validateAndConstructHttpBindings(queryParamNames, null, message, messageTypes, false))
105129
.setBodyParameters(
106-
validateAndConstructHttpBindings(bodyParamNames, message, messageTypes, false))
130+
validateAndConstructHttpBindings(bodyParamNames, null, message, messageTypes, false))
107131
.setIsAsteriskBody(body.equals(ASTERISK))
108132
.build();
109133
}
110134

111135
private static Set<HttpBinding> validateAndConstructHttpBindings(
112136
Set<String> paramNames,
137+
Map<String, String> fieldToAliasMap,
113138
Message inputMessage,
114139
Map<String, Message> messageTypes,
115140
boolean isPath) {
141+
142+
if (fieldToAliasMap != null) {
143+
paramNames = fieldToAliasMap.keySet();
144+
}
145+
116146
ImmutableSortedSet.Builder<HttpBinding> httpBindings = ImmutableSortedSet.naturalOrder();
117147
for (String paramName : paramNames) {
118148
// Handle foo.bar cases by descending into the subfields.
119149
String[] subFields = paramName.split("\\.");
120150
if (inputMessage == null) {
121-
httpBindings.add(HttpBinding.create(paramName, false));
151+
httpBindings.add(
152+
HttpBinding.create(paramName, false, getAlias(paramName, fieldToAliasMap)));
122153
continue;
123154
}
124155
Message nestedMessage = inputMessage;
@@ -138,13 +169,23 @@ private static Set<HttpBinding> validateAndConstructHttpBindings(
138169
checkHttpFieldIsValid(subFieldName, nestedMessage, !isPath);
139170
}
140171
Field field = nestedMessage.fieldMap().get(subFieldName);
141-
httpBindings.add(HttpBinding.create(paramName, field.isProto3Optional()));
172+
httpBindings.add(
173+
HttpBinding.create(
174+
paramName, field.isProto3Optional(), getAlias(paramName, fieldToAliasMap)));
142175
}
143176
}
144177
}
145178
return httpBindings.build();
146179
}
147180

181+
private static String getAlias(String paramName, Map<String, String> fieldToAliasMap) {
182+
if (fieldToAliasMap != null && fieldToAliasMap.containsKey(paramName)) {
183+
return fieldToAliasMap.get(paramName);
184+
} else {
185+
return paramName;
186+
}
187+
}
188+
148189
private static String getHttpVerbPattern(HttpRule httpRule) {
149190
PatternCase patternCase = httpRule.getPatternCase();
150191
switch (patternCase) {

src/test/java/com/google/api/generator/gapic/composer/grpc/goldens/GrpcTestingStub.golden

+1-1
Original file line numberDiff line numberDiff line change
@@ -221,7 +221,7 @@ public class GrpcTestingStub extends TestingStub {
221221
.setParamsExtractor(
222222
request -> {
223223
ImmutableMap.Builder<String, String> params = ImmutableMap.builder();
224-
params.put("name", String.valueOf(request.getName()));
224+
params.put("rename", String.valueOf(request.getName()));
225225
return params.build();
226226
})
227227
.build();

src/test/java/com/google/api/generator/gapic/protoparser/HttpRuleParserTest.java

+19
Original file line numberDiff line numberDiff line change
@@ -91,4 +91,23 @@ public void parseHttpAnnotation_missingFieldFromMessage() {
9191
assertThrows(
9292
IllegalStateException.class, () -> HttpRuleParser.parse(rpcMethod, inputMessage, messages));
9393
}
94+
95+
@Test
96+
public void parseRoutingAnnotation_alias() {
97+
FileDescriptor testingFileDescriptor = TestingOuterClass.getDescriptor();
98+
ServiceDescriptor testingService = testingFileDescriptor.getServices().get(0);
99+
assertEquals("Testing", testingService.getName());
100+
101+
Map<String, Message> messages = Parser.parseMessages(testingFileDescriptor);
102+
103+
// GetTest method.
104+
MethodDescriptor rpcMethod = testingService.getMethods().get(5);
105+
Message inputMessage = messages.get("com.google.showcase.v1beta1.GetTestRequest");
106+
HttpBindings httpBindings = HttpRuleParser.parse(rpcMethod, inputMessage, messages);
107+
assertThat(
108+
httpBindings.pathParameters().stream()
109+
.map(binding -> binding.name() + " -> " + binding.alias())
110+
.collect(Collectors.toList()))
111+
.containsExactly("name -> rename");
112+
}
94113
}

src/test/java/com/google/api/generator/gapic/testdata/BUILD.bazel

+1
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,7 @@ proto_library(
5454
],
5555
deps = [
5656
"@com_google_googleapis//google/api:annotations_proto",
57+
"@com_google_googleapis//google/api:routing_proto",
5758
"@com_google_googleapis//google/api:client_proto",
5859
"@com_google_googleapis//google/api:field_behavior_proto",
5960
"@com_google_googleapis//google/api:resource_proto",

src/test/java/com/google/api/generator/gapic/testdata/testing.proto

+7
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
syntax = "proto3";
1616

1717
import "google/api/annotations.proto";
18+
import "google/api/routing.proto";
1819
import "google/api/client.proto";
1920
import "google/api/resource.proto";
2021
import "google/protobuf/empty.proto";
@@ -78,6 +79,12 @@ service Testing {
7879
option (google.api.http) = {
7980
get: "/v1beta1/{name=tests/*}"
8081
};
82+
option (google.api.routing) = {
83+
routing_parameters {
84+
field: "name"
85+
path_template: "/v1beta1/{rename=tests/*}"
86+
}
87+
};
8188
option (google.api.method_signature) = "name";
8289
}
8390

test/integration/goldens/redis/com/google/cloud/redis/v1beta1/CloudRedisClient.java

+13-12
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,7 @@
5757
* `/projects/{project_id}/locations/{location_id}/instances/{instance_id}`
5858
* </ul>
5959
*
60-
* <p>Note that location_id must be refering to a GCP `region`; for example:
60+
* <p>Note that location_id must be referring to a GCP `region`; for example:
6161
*
6262
* <ul>
6363
* <li>`projects/redpepper-1290/locations/us-central1/instances/my-redis`
@@ -472,7 +472,7 @@ public final UnaryCallable<GetInstanceRequest, Instance> getInstanceCallable() {
472472
*
473473
* <p>The creation is executed asynchronously and callers may check the returned operation to
474474
* track its progress. Once the operation is completed the Redis instance will be fully
475-
* functional. Completed longrunning.Operation will contain the new instance object in the
475+
* functional. The completed longrunning.Operation will contain the new instance object in the
476476
* response field.
477477
*
478478
* <p>The returned operation is automatically deleted after a few hours, so there is no need to
@@ -524,7 +524,7 @@ public final OperationFuture<Instance, Any> createInstanceAsync(
524524
*
525525
* <p>The creation is executed asynchronously and callers may check the returned operation to
526526
* track its progress. Once the operation is completed the Redis instance will be fully
527-
* functional. Completed longrunning.Operation will contain the new instance object in the
527+
* functional. The completed longrunning.Operation will contain the new instance object in the
528528
* response field.
529529
*
530530
* <p>The returned operation is automatically deleted after a few hours, so there is no need to
@@ -576,7 +576,7 @@ public final OperationFuture<Instance, Any> createInstanceAsync(
576576
*
577577
* <p>The creation is executed asynchronously and callers may check the returned operation to
578578
* track its progress. Once the operation is completed the Redis instance will be fully
579-
* functional. Completed longrunning.Operation will contain the new instance object in the
579+
* functional. The completed longrunning.Operation will contain the new instance object in the
580580
* response field.
581581
*
582582
* <p>The returned operation is automatically deleted after a few hours, so there is no need to
@@ -612,7 +612,7 @@ public final OperationFuture<Instance, Any> createInstanceAsync(CreateInstanceRe
612612
*
613613
* <p>The creation is executed asynchronously and callers may check the returned operation to
614614
* track its progress. Once the operation is completed the Redis instance will be fully
615-
* functional. Completed longrunning.Operation will contain the new instance object in the
615+
* functional. The completed longrunning.Operation will contain the new instance object in the
616616
* response field.
617617
*
618618
* <p>The returned operation is automatically deleted after a few hours, so there is no need to
@@ -649,7 +649,7 @@ public final OperationFuture<Instance, Any> createInstanceAsync(CreateInstanceRe
649649
*
650650
* <p>The creation is executed asynchronously and callers may check the returned operation to
651651
* track its progress. Once the operation is completed the Redis instance will be fully
652-
* functional. Completed longrunning.Operation will contain the new instance object in the
652+
* functional. The completed longrunning.Operation will contain the new instance object in the
653653
* response field.
654654
*
655655
* <p>The returned operation is automatically deleted after a few hours, so there is no need to
@@ -696,7 +696,8 @@ public final UnaryCallable<CreateInstanceRequest, Operation> createInstanceCalla
696696
* @param updateMask Required. Mask of fields to update. At least one path must be supplied in
697697
* this field. The elements of the repeated paths field may only include these fields from
698698
* [Instance][google.cloud.redis.v1beta1.Instance]:
699-
* <p>&#42; `displayName` &#42; `labels` &#42; `memorySizeGb` &#42; `redisConfig`
699+
* <p>&#42; `displayName` &#42; `labels` &#42; `memorySizeGb` &#42; `redisConfig` &#42;
700+
* `replica_count`
700701
* @param instance Required. Update description. Only fields specified in update_mask are updated.
701702
* @throws com.google.api.gax.rpc.ApiException if the remote call fails
702703
*/
@@ -1164,7 +1165,7 @@ public final UnaryCallable<ExportInstanceRequest, Operation> exportInstanceCalla
11641165

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

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

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

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

12811282
// AUTO-GENERATED DOCUMENTATION AND METHOD.
12821283
/**
1283-
* Initiates a failover of the master node to current replica node for a specific STANDARD tier
1284+
* Initiates a failover of the primary node to current replica node for a specific STANDARD tier
12841285
* Cloud Memorystore for Redis instance.
12851286
*
12861287
* <p>Sample code:

0 commit comments

Comments
 (0)