Skip to content

Commit f46480a

Browse files
authored
feat(gapic-generator-java): Extract resource name heuristicly (#12207)
This PR extracts resource name heuristically if there are no resource_reference configured for a request. This logic only applies to the following proto packages: - `google.cloud.compute.**` - `google.cloud.sql.**` - `google.cloud.bigquery.**` The exact logic is described in go/client-libraries:destination-resource-name. In a nutshell, a canonical resource name is extracted from the template by finding the version literal, then finding the last binding that is a literal/binding pair or named binding, and then extracting the segments between the version literal and the last binding (inclusive). The logic is mostly implemented in [PathTemplate](https://github.com/googleapis/google-cloud-java/blob/f63e3eb3efb2eea25b41dc8ef6e282b3b1e6ef06/sdk-platform-java/api-common-java/src/main/java/com/google/api/pathtemplate/PathTemplate.java#L301-L415), and used by [AbstractTransportServiceStubClassComposer#createResourceNameExtractorClassInstance](https://github.com/googleapis/google-cloud-java/blob/2e56ceeb064a9f6ea2c08d30b57011e3b19e2c63/sdk-platform-java/gapic-generator-java/src/main/java/com/google/api/generator/gapic/composer/common/AbstractTransportServiceStubClassComposer.java#L1564-L1673). The generated code would be called in the same way as a regular [resourceNameExtractor](https://github.com/googleapis/google-cloud-java/blob/2e56ceeb064a9f6ea2c08d30b57011e3b19e2c63/sdk-platform-java/gax-java/gax/src/main/java/com/google/api/gax/rpc/ResourceNameExtractor.java) by [TracedUnaryCallable](https://github.com/googleapis/google-cloud-java/blob/39133b59eae5ff3b8fe08efbe14a402de96f9e5c/sdk-platform-java/gax-java/gax/src/main/java/com/google/api/gax/tracing/TracedUnaryCallable.java#L113-L122).
1 parent 556e2f7 commit f46480a

File tree

9 files changed

+1399
-41
lines changed

9 files changed

+1399
-41
lines changed

sdk-platform-java/api-common-java/src/main/java/com/google/api/pathtemplate/PathTemplate.java

Lines changed: 123 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -296,6 +296,129 @@ public Set<String> vars() {
296296
return bindings.keySet();
297297
}
298298

299+
/** Returns the set of resource literals. A resource literal is a literal followed by a binding */
300+
// For example, projects/{project} is a literal/binding pair and projects is a resource literal.
301+
public Set<String> getResourceLiterals() {
302+
Set<String> canonicalSegments = new java.util.LinkedHashSet<>();
303+
boolean inBinding = false;
304+
for (int i = 0; i < segments.size(); i++) {
305+
Segment seg = segments.get(i);
306+
if (seg.kind() == SegmentKind.BINDING) {
307+
inBinding = true;
308+
} else if (seg.kind() == SegmentKind.END_BINDING) {
309+
inBinding = false;
310+
} else if (seg.kind() == SegmentKind.LITERAL) {
311+
String value = seg.value();
312+
// Skipping version literals such as v1, v1beta1
313+
if (value.matches("^v\\d+[a-zA-Z0-9]*$")) {
314+
continue;
315+
}
316+
if (inBinding) {
317+
// This is for extracting "projects" and "locations" from named binding
318+
// {name=projects/*/locations/*}
319+
canonicalSegments.add(value);
320+
} else if (i + 1 < segments.size() && segments.get(i + 1).kind() == SegmentKind.BINDING) {
321+
// This is for regular cases projects/{project}/locations/{location}
322+
canonicalSegments.add(value);
323+
}
324+
}
325+
}
326+
return canonicalSegments;
327+
}
328+
329+
/**
330+
* Returns the canonical resource name string. A canonical resource name is extracted from the
331+
* template by finding the version literal, then finding the last binding that is a
332+
* literal/binding pair or named binding, and then extracting the segments between the version
333+
* literal and the last binding (inclusive). This is a heuristic method that should only be used
334+
* for allowlisted services. There are also known gaps, such as the fact that it does not work
335+
* properly for singleton resources.
336+
*/
337+
// For example, projects/{project} is a literal/binding pair. {bar=projects/*/locations/*/bars/*}
338+
// is a named binding.
339+
// If a template is /compute/v1/projects/{project}/locations/{location}, known resource literals
340+
// are "projects" and "locations", the canonical resource name would be
341+
// projects/{project}/locations/{location}. See unit tests for all cases.
342+
public String getCanonicalResourceName(Set<String> knownResourceLiterals) {
343+
if (knownResourceLiterals == null) {
344+
return "";
345+
}
346+
347+
int startIndex = 0;
348+
for (int i = 0; i < segments.size(); i++) {
349+
Segment seg = segments.get(i);
350+
if (seg.kind() == SegmentKind.LITERAL) {
351+
String value = seg.value();
352+
if (value.matches("^v\\d+[a-zA-Z0-9]*$")) {
353+
startIndex = i + 1;
354+
break;
355+
}
356+
}
357+
}
358+
359+
int lastValidEndBindingIndex = -1;
360+
// Iterate from the end of the segments to find the last valid resource binding.
361+
// Searching backwards allows us to stop immediately once the last valid pair is found.
362+
for (int i = segments.size() - 1; i >= 0; i--) {
363+
Segment seg = segments.get(i);
364+
365+
// We are looking for the end of a binding (e.g., "}" in "{project}" or "{name=projects/*}")
366+
if (seg.kind() == SegmentKind.END_BINDING) {
367+
int bindingStartIndex = -1;
368+
int literalCountInBinding = 0;
369+
boolean isValidPair = false;
370+
371+
// Traverse backwards to find the start of this specific binding
372+
// and count the literals captured inside it.
373+
for (int j = i - 1; j >= 0; j--) {
374+
Segment innerSeg = segments.get(j);
375+
if (innerSeg.kind() == SegmentKind.BINDING) {
376+
bindingStartIndex = j;
377+
break;
378+
} else if (innerSeg.kind() == SegmentKind.LITERAL
379+
|| innerSeg.kind() == SegmentKind.PATH_WILDCARD) {
380+
literalCountInBinding++;
381+
}
382+
}
383+
384+
if (bindingStartIndex != -1) {
385+
// 1. If the binding contains any literals, it is considered a valid named resource
386+
// binding.
387+
if (literalCountInBinding > 0) {
388+
isValidPair = true;
389+
} else if (bindingStartIndex > 0) {
390+
// 2. For simple bindings like "{project}", the binding itself has no inner literal
391+
// resources.
392+
// Instead, we check if the literal segment immediately preceding it (e.g., "projects/")
393+
// is a known resource.
394+
Segment prevSeg = segments.get(bindingStartIndex - 1);
395+
if (prevSeg.kind() == SegmentKind.LITERAL
396+
&& knownResourceLiterals.contains(prevSeg.value())) {
397+
isValidPair = true;
398+
}
399+
}
400+
401+
if (isValidPair) {
402+
// We successfully found the last valid binding! Record its end index and terminate the
403+
// search.
404+
lastValidEndBindingIndex = i;
405+
break;
406+
}
407+
// The current binding wasn't a valid resource pair.
408+
// Skip over all inner segments of this invalid binding so we don't evaluate them again.
409+
i = bindingStartIndex;
410+
}
411+
}
412+
}
413+
414+
if (lastValidEndBindingIndex == -1 || lastValidEndBindingIndex < startIndex) {
415+
return "";
416+
}
417+
418+
List<Segment> canonicalSegments = segments.subList(startIndex, lastValidEndBindingIndex + 1);
419+
return toSyntax(canonicalSegments, true).replace("=*}", "}");
420+
}
421+
299422
/**
300423
* Returns a template for the parent of this template.
301424
*

sdk-platform-java/api-common-java/src/test/java/com/google/api/pathtemplate/PathTemplateTest.java

Lines changed: 166 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,9 @@
3131
package com.google.api.pathtemplate;
3232

3333
import com.google.common.collect.ImmutableMap;
34+
import com.google.common.collect.ImmutableSet;
3435
import com.google.common.truth.Truth;
36+
import java.util.HashSet;
3537
import java.util.Map;
3638
import java.util.Set;
3739
import java.util.stream.Stream;
@@ -894,6 +896,170 @@ void testTemplateWithMultipleSimpleBindings() {
894896
Truth.assertThat(url).isEqualTo("v1/shelves/s1/books/b1");
895897
}
896898

899+
@Test
900+
void testGetResourceLiterals_simplePath() {
901+
PathTemplate template =
902+
PathTemplate.create("/compute/v1/projects/{project}/locations/{location}/widgets/{widget}");
903+
Truth.assertThat(template.getResourceLiterals())
904+
.containsExactly("projects", "locations", "widgets");
905+
}
906+
907+
@Test
908+
void testGetResourceLiterals_multipleLiterals() {
909+
PathTemplate template =
910+
PathTemplate.create(
911+
"/compute/v1/projects/{project}/global/locations/{location}/widgets/{widget}");
912+
Truth.assertThat(template.getResourceLiterals())
913+
.containsExactly("projects", "locations", "widgets");
914+
}
915+
916+
@Test
917+
void testGetResourceLiterals_regexPath() {
918+
PathTemplate template =
919+
PathTemplate.create("v1/projects/{project=projects/*}/instances/{instance_id=instances/*}");
920+
Truth.assertThat(template.getResourceLiterals()).containsExactly("projects", "instances");
921+
}
922+
923+
@Test
924+
void testGetResourceLiterals_onlyNonResourceLiterals() {
925+
PathTemplate template = PathTemplate.create("compute/v1/projects");
926+
Truth.assertThat(template.getResourceLiterals()).isEmpty();
927+
}
928+
929+
@Test
930+
void testGetResourceLiterals_nameBinding() {
931+
PathTemplate template = PathTemplate.create("v1/{name=projects/*/instances/*}");
932+
Truth.assertThat(template.getResourceLiterals()).containsExactly("projects", "instances");
933+
}
934+
935+
@Test
936+
void testGetResourceLiterals_complexResourceId() {
937+
PathTemplate template = PathTemplate.create("projects/{project}/zones/{zone_a}~{zone_b}");
938+
Truth.assertThat(template.getResourceLiterals()).containsExactly("projects", "zones");
939+
}
940+
941+
@Test
942+
void testGetResourceLiterals_customVerb() {
943+
PathTemplate template = PathTemplate.create("projects/{project}/instances/{instance}:execute");
944+
Truth.assertThat(template.getResourceLiterals()).containsExactly("projects", "instances");
945+
}
946+
947+
@Test
948+
void testGetCanonicalResourceName_namedBindingsSimple() {
949+
Set<String> moreKnownResources = ImmutableSet.of("projects", "locations", "bars");
950+
PathTemplate template = PathTemplate.create("/v1/{bar=projects/*/locations/*/bars/*}");
951+
Truth.assertThat(template.getCanonicalResourceName(moreKnownResources))
952+
.isEqualTo("{bar=projects/*/locations/*/bars/*}");
953+
}
954+
955+
@Test
956+
void testGetCanonicalResourceName_namedBindingsWithUnknownResource() {
957+
Set<String> knownResources = ImmutableSet.of();
958+
PathTemplate template = PathTemplate.create("/v1/{bar=projects/*/locations/*/unknown/*}");
959+
Truth.assertThat(template.getCanonicalResourceName(knownResources))
960+
.isEqualTo("{bar=projects/*/locations/*/unknown/*}");
961+
}
962+
963+
@Test
964+
void testGetCanonicalResourceName_simplePath() {
965+
Set<String> knownResources = ImmutableSet.of("projects", "locations", "instances", "widgets");
966+
PathTemplate template =
967+
PathTemplate.create("/compute/v1/projects/{project}/locations/{location}/widgets/{widget}");
968+
Truth.assertThat(template.getCanonicalResourceName(knownResources))
969+
.isEqualTo("projects/{project}/locations/{location}/widgets/{widget}");
970+
}
971+
972+
@Test
973+
void testGetCanonicalResourceName_v1beta1WithSimplePath() {
974+
Set<String> knownResources = ImmutableSet.of("projects", "locations", "instances", "widgets");
975+
PathTemplate template =
976+
PathTemplate.create(
977+
"/compute/v1beta1/projects/{project}/locations/{location}/widgets/{widget}");
978+
Truth.assertThat(template.getCanonicalResourceName(knownResources))
979+
.isEqualTo("projects/{project}/locations/{location}/widgets/{widget}");
980+
}
981+
982+
@Test
983+
void testGetCanonicalResourceName_regexVariables() {
984+
Set<String> knownResources = ImmutableSet.of("projects", "locations", "instances", "widgets");
985+
PathTemplate template =
986+
PathTemplate.create("v1/projects/{project=projects/*}/instances/{instance_id=instances/*}");
987+
Truth.assertThat(template.getCanonicalResourceName(knownResources))
988+
.isEqualTo("projects/{project=projects/*}/instances/{instance_id=instances/*}");
989+
}
990+
991+
@Test
992+
void testGetCanonicalResourceName_noVariables() {
993+
Set<String> knownResources = ImmutableSet.of("projects", "locations", "instances", "widgets");
994+
PathTemplate template = PathTemplate.create("v1/projects/locations");
995+
Truth.assertThat(template.getCanonicalResourceName(knownResources)).isEmpty();
996+
}
997+
998+
@Test
999+
void testGetCanonicalResourceName_unknownResource() {
1000+
Set<String> knownResources = ImmutableSet.of("projects", "locations", "instances", "widgets");
1001+
PathTemplate template =
1002+
PathTemplate.create("v1/projects/{project}/unknownResource/{unknownResource}");
1003+
Truth.assertThat(template.getCanonicalResourceName(knownResources))
1004+
.isEqualTo("projects/{project}");
1005+
}
1006+
1007+
@Test
1008+
void testGetCanonicalResourceName_customVerb() {
1009+
Set<String> knownResources = ImmutableSet.of("projects", "locations", "instances", "widgets");
1010+
PathTemplate template = PathTemplate.create("projects/{project}/instances/{instance}:execute");
1011+
Truth.assertThat(template.getCanonicalResourceName(knownResources))
1012+
.isEqualTo("projects/{project}/instances/{instance}");
1013+
}
1014+
1015+
@Test
1016+
void testGetCanonicalResourceName_nameBindingMixedWithSimpleBinding() {
1017+
Set<String> knownResources = ImmutableSet.of("projects", "locations", "instances", "widgets");
1018+
PathTemplate template =
1019+
PathTemplate.create("v1/{field=projects/*/instances/*}/actions/{action}");
1020+
Truth.assertThat(template.getCanonicalResourceName(knownResources))
1021+
.isEqualTo("{field=projects/*/instances/*}");
1022+
}
1023+
1024+
@Test
1025+
void testGetCanonicalResourceName_multipleLiteralsWithSimpleBinding() {
1026+
Set<String> knownResources = ImmutableSet.of("actions");
1027+
PathTemplate template = PathTemplate.create("v1/locations/global/actions/{action}");
1028+
Truth.assertThat(template.getCanonicalResourceName(knownResources))
1029+
.isEqualTo("locations/global/actions/{action}");
1030+
}
1031+
1032+
@Test
1033+
void testGetCanonicalResourceName_multipleLiteralsWithMultipleBindings() {
1034+
Set<String> knownResources = ImmutableSet.of("instances", "actions");
1035+
PathTemplate template =
1036+
PathTemplate.create("v1/locations/global/instances/{instance}/actions/{action}");
1037+
Truth.assertThat(template.getCanonicalResourceName(knownResources))
1038+
.isEqualTo("locations/global/instances/{instance}/actions/{action}");
1039+
}
1040+
1041+
@Test
1042+
void testGetCanonicalResourceName_multipleLiteralsBetweenMultipleBindings() {
1043+
Set<String> knownResources = ImmutableSet.of("instances", "actions");
1044+
PathTemplate template =
1045+
PathTemplate.create("v1/instances/{instance}/locations/global/actions/{action}");
1046+
Truth.assertThat(template.getCanonicalResourceName(knownResources))
1047+
.isEqualTo("instances/{instance}/locations/global/actions/{action}");
1048+
}
1049+
1050+
@Test
1051+
void testGetCanonicalResourceName_nullKnownResources() {
1052+
PathTemplate template =
1053+
PathTemplate.create("v1/projects/{project}/locations/{location}/widgets/{widget}");
1054+
Truth.assertThat(template.getCanonicalResourceName(null)).isEmpty();
1055+
}
1056+
1057+
@Test
1058+
void testGetCanonicalResourceName_pathWildCard() {
1059+
PathTemplate template = PathTemplate.create("/v1/{resource=**}:setIamPolicy");
1060+
Truth.assertThat(template.getCanonicalResourceName(new HashSet<>())).isEqualTo("{resource=**}");
1061+
}
1062+
8971063
private static void assertPositionalMatch(Map<String, String> match, String... expected) {
8981064
Truth.assertThat(match).isNotNull();
8991065
int i = 0;

0 commit comments

Comments
 (0)