Skip to content
Merged
Show file tree
Hide file tree
Changes from 18 commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
3b82880
fix: Handle null server address
blakeli0 Mar 24, 2026
da23a42
Filter heuristic resource name extraction by proto package
blakeli0 Mar 26, 2026
366075b
Revert "fix: Handle null server address"
blakeli0 Mar 26, 2026
2e56cee
Remove empty line at the end of EndpointContextTest.java
blakeli0 Mar 26, 2026
4efed5a
tests: fix tests
blakeli0 Mar 26, 2026
409522d
tests: update integration tests goldens.
blakeli0 Mar 26, 2026
877fec6
fix: Update the heuristic logic per latest discussion.
blakeli0 Mar 27, 2026
fbd911f
Merge branch 'main' into feature/heuristic-filter
blakeli0 Mar 27, 2026
1b4b5e9
fix: update docs.
blakeli0 Mar 27, 2026
c18077a
fix: update version logic
blakeli0 Mar 27, 2026
523573e
fix: update javadocs
blakeli0 Mar 27, 2026
3edd75f
fix: Revert necessary changes.
blakeli0 Mar 27, 2026
a819980
fix: fix integration tests
blakeli0 Mar 27, 2026
ad9bc73
fix: fix integration tests
blakeli0 Mar 27, 2026
3bf58f0
docs: add comments.
blakeli0 Mar 27, 2026
e5142f1
fix: simplify logics to get the start of the binding.
blakeli0 Mar 28, 2026
67168ad
fix: simplify logics to get the start of the binding.
blakeli0 Mar 28, 2026
f63e3eb
fix: format
blakeli0 Mar 28, 2026
d49e0f1
Merge branch 'main' into feature/heuristic-filter
blakeli0 Mar 30, 2026
57dedd3
fix: Add support for path wildcard.
blakeli0 Mar 30, 2026
5dc67eb
fix: update docs
blakeli0 Mar 30, 2026
9279f60
Merge branch 'main' into feature/heuristic-filter
blakeli0 Mar 30, 2026
72bd55a
Merge branch 'main' into feature/heuristic-filter
blakeli0 Mar 31, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -296,6 +296,124 @@ public Set<String> vars() {
return bindings.keySet();
}

/** Returns the set of resource literals. A resource literal is a literal followed by a binding */
// For example, projects/{project} is a literal/binding pair and projects is a resource literal.
Comment thread
lqiu96 marked this conversation as resolved.
public Set<String> getResourceLiterals() {
Set<String> canonicalSegments = new java.util.LinkedHashSet<>();
boolean inBinding = false;
for (int i = 0; i < segments.size(); i++) {
Segment seg = segments.get(i);
if (seg.kind() == SegmentKind.BINDING) {
inBinding = true;
Comment thread
lqiu96 marked this conversation as resolved.
} else if (seg.kind() == SegmentKind.END_BINDING) {
inBinding = false;
} else if (seg.kind() == SegmentKind.LITERAL) {
String value = seg.value();
if (value.matches("^v\\d+[a-zA-Z0-9]*$")) { // just in case
Copy link
Copy Markdown
Member

@lqiu96 lqiu96 Mar 30, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

From the AIP, I believe the regex should be:

For collection ID: [a-z][a-zA-Z0-9]*
From resource ID: ^[a-z]([a-z0-9-]{0,61}[a-z0-9])?$

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is to skip version literals such as v1, v1beta1.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh I misread this. In that case, can you add a small comment that this is to skip the version in the http path?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added.

continue;
}
if (inBinding) {
// This is for extracting "projects" and "locations" from named binding
// {name=projects/*/locations/*}
canonicalSegments.add(value);
} else if (i + 1 < segments.size() && segments.get(i + 1).kind() == SegmentKind.BINDING) {
// This is for regular cases projects/{project}/locations/{location}
canonicalSegments.add(value);
}
}
Comment thread
diegomarquezp marked this conversation as resolved.
}
return canonicalSegments;
}

/**
* Returns the canonical resource name string. 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.
*/
// For example, projects/{project} is a literal/binding pair. {bar=projects/*/locations/*/bars/*}
Comment thread
diegomarquezp marked this conversation as resolved.
// is a named binding.
// If a template is /compute/v1/projects/{project}/locations/{location}, known resources are
// "projects" and "locations", the canonical resource name is
// projects/{project}/locations/{location}. See unit tests for all cases.
Comment thread
lqiu96 marked this conversation as resolved.
public String getCanonicalResourceName(Set<String> knownResources) {
Copy link
Copy Markdown
Member

@lqiu96 lqiu96 Mar 30, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What is the format of the knownResources? Does it come in as the resource pattern (e.g. something like projects/{project}/locations/{location})?

I believe this would be different from the url format which would be projects/*/locations/*.

Just want to double check this: when the logic compares against the previous segment, it takes care of checking the possible wildcard values with the resource pattern that has no wildcards?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It actually should be renamed to knownResourceLiterals, it includes all resource literals (extracted by previous method getResourceLiterals) from the current service.

if (knownResources == null) {
return "";
}

int startIndex = 0;
for (int i = 0; i < segments.size(); i++) {
Segment seg = segments.get(i);
if (seg.kind() == SegmentKind.LITERAL) {
Comment thread
lqiu96 marked this conversation as resolved.
String value = seg.value();
if (value.matches("^v\\d+[a-zA-Z0-9]*$")) {
startIndex = i + 1;
Comment thread
diegomarquezp marked this conversation as resolved.
break;
}
}
}

int lastValidEndBindingIndex = -1;
// Iterate from the end of the segments to find the last valid resource binding.
// Searching backwards allows us to stop immediately once the last valid pair is found.
for (int i = segments.size() - 1; i >= 0; i--) {
Segment seg = segments.get(i);

// We are looking for the end of a binding (e.g., "}" in "{project}" or "{name=projects/*}")
if (seg.kind() == SegmentKind.END_BINDING) {
int bindingStartIndex = -1;
int literalCountInBinding = 0;
boolean isValidPair = false;

// Traverse backwards to find the start of this specific binding
// and count the literals captured inside it.
for (int j = i - 1; j >= 0; j--) {
Segment innerSeg = segments.get(j);
if (innerSeg.kind() == SegmentKind.BINDING) {
bindingStartIndex = j;
break;
} else if (innerSeg.kind() == SegmentKind.LITERAL) {
literalCountInBinding++;
}
}

if (bindingStartIndex != -1) {
// 1. If the binding contains any literals, it is considered a valid named resource
// binding.
if (literalCountInBinding > 0) {
isValidPair = true;
} else if (bindingStartIndex > 0) {
// 2. For simple bindings like "{project}", the binding itself has no inner literal
// resources.
// Instead, we check if the literal segment immediately preceding it (e.g., "projects/")
// is a known resource.
Segment prevSeg = segments.get(bindingStartIndex - 1);
if (prevSeg.kind() == SegmentKind.LITERAL && knownResources.contains(prevSeg.value())) {
isValidPair = true;
}
}

if (isValidPair) {
// We successfully found the last valid binding! Record its end index and terminate the
// search.
lastValidEndBindingIndex = i;
break;
}
// The current binding wasn't a valid resource pair.
// Skip over all inner segments of this invalid binding so we don't evaluate them again.
i = bindingStartIndex;
}
}
}

if (lastValidEndBindingIndex == -1 || lastValidEndBindingIndex < startIndex) {
return "";
}

List<Segment> canonicalSegments = segments.subList(startIndex, lastValidEndBindingIndex + 1);
return toSyntax(canonicalSegments, true).replace("=*}", "}");
Comment thread
lqiu96 marked this conversation as resolved.
}

/**
* Returns a template for the parent of this template.
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@
package com.google.api.pathtemplate;

import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableSet;
import com.google.common.truth.Truth;
import java.util.Map;
import java.util.Set;
Expand Down Expand Up @@ -894,6 +895,164 @@ void testTemplateWithMultipleSimpleBindings() {
Truth.assertThat(url).isEqualTo("v1/shelves/s1/books/b1");
}

@Test
void testGetResourceLiterals_simplePath() {
PathTemplate template =
PathTemplate.create("/compute/v1/projects/{project}/locations/{location}/widgets/{widget}");
Truth.assertThat(template.getResourceLiterals())
.containsExactly("projects", "locations", "widgets");
}
Comment thread
blakeli0 marked this conversation as resolved.

@Test
void testGetResourceLiterals_multipleLiterals() {
PathTemplate template =
PathTemplate.create(
"/compute/v1/projects/{project}/global/locations/{location}/widgets/{widget}");
Truth.assertThat(template.getResourceLiterals())
.containsExactly("projects", "locations", "widgets");
}

@Test
void testGetResourceLiterals_regexPath() {
PathTemplate template =
PathTemplate.create("v1/projects/{project=projects/*}/instances/{instance_id=instances/*}");
Truth.assertThat(template.getResourceLiterals()).containsExactly("projects", "instances");
}

@Test
void testGetResourceLiterals_onlyNonResourceLiterals() {
PathTemplate template = PathTemplate.create("compute/v1/projects");
Truth.assertThat(template.getResourceLiterals()).isEmpty();
}

@Test
void testGetResourceLiterals_nameBinding() {
PathTemplate template = PathTemplate.create("v1/{name=projects/*/instances/*}");
Truth.assertThat(template.getResourceLiterals()).containsExactly("projects", "instances");
}

@Test
void testGetResourceLiterals_complexResourceId() {
PathTemplate template = PathTemplate.create("projects/{project}/zones/{zone_a}~{zone_b}");
Truth.assertThat(template.getResourceLiterals()).containsExactly("projects", "zones");
}

@Test
void testGetResourceLiterals_customVerb() {
PathTemplate template = PathTemplate.create("projects/{project}/instances/{instance}:execute");
Truth.assertThat(template.getResourceLiterals()).containsExactly("projects", "instances");
}

@Test
void testGetCanonicalResourceName_namedBindingsSimple() {
Set<String> moreKnownResources = ImmutableSet.of("projects", "locations", "bars");
PathTemplate template = PathTemplate.create("/v1/{bar=projects/*/locations/*/bars/*}");
Truth.assertThat(template.getCanonicalResourceName(moreKnownResources))
.isEqualTo("{bar=projects/*/locations/*/bars/*}");
}

@Test
void testGetCanonicalResourceName_namedBindingsWithUnknownResource() {
Set<String> knownResources = ImmutableSet.of();
PathTemplate template = PathTemplate.create("/v1/{bar=projects/*/locations/*/unknown/*}");
Truth.assertThat(template.getCanonicalResourceName(knownResources))
.isEqualTo("{bar=projects/*/locations/*/unknown/*}");
}

@Test
void testGetCanonicalResourceName_simplePath() {
Set<String> knownResources = ImmutableSet.of("projects", "locations", "instances", "widgets");
PathTemplate template =
PathTemplate.create("/compute/v1/projects/{project}/locations/{location}/widgets/{widget}");
Truth.assertThat(template.getCanonicalResourceName(knownResources))
.isEqualTo("projects/{project}/locations/{location}/widgets/{widget}");
}

@Test
void testGetCanonicalResourceName_v1beta1WithSimplePath() {
Set<String> knownResources = ImmutableSet.of("projects", "locations", "instances", "widgets");
PathTemplate template =
PathTemplate.create(
"/compute/v1beta1/projects/{project}/locations/{location}/widgets/{widget}");
Truth.assertThat(template.getCanonicalResourceName(knownResources))
.isEqualTo("projects/{project}/locations/{location}/widgets/{widget}");
}

@Test
void testGetCanonicalResourceName_regexVariables() {
Set<String> knownResources = ImmutableSet.of("projects", "locations", "instances", "widgets");
PathTemplate template =
PathTemplate.create("v1/projects/{project=projects/*}/instances/{instance_id=instances/*}");
Truth.assertThat(template.getCanonicalResourceName(knownResources))
.isEqualTo("projects/{project=projects/*}/instances/{instance_id=instances/*}");
}

@Test
void testGetCanonicalResourceName_noVariables() {
Set<String> knownResources = ImmutableSet.of("projects", "locations", "instances", "widgets");
PathTemplate template = PathTemplate.create("v1/projects/locations");
Truth.assertThat(template.getCanonicalResourceName(knownResources)).isEmpty();
}

@Test
void testGetCanonicalResourceName_unknownResource() {
Set<String> knownResources = ImmutableSet.of("projects", "locations", "instances", "widgets");
PathTemplate template =
PathTemplate.create("v1/projects/{project}/unknownResource/{unknownResource}");
Truth.assertThat(template.getCanonicalResourceName(knownResources))
.isEqualTo("projects/{project}");
}

@Test
void testGetCanonicalResourceName_customVerb() {
Set<String> knownResources = ImmutableSet.of("projects", "locations", "instances", "widgets");
PathTemplate template = PathTemplate.create("projects/{project}/instances/{instance}:execute");
Truth.assertThat(template.getCanonicalResourceName(knownResources))
.isEqualTo("projects/{project}/instances/{instance}");
}

@Test
void testGetCanonicalResourceName_nameBindingMixedWithSimpleBinding() {
Set<String> knownResources = ImmutableSet.of("projects", "locations", "instances", "widgets");
PathTemplate template =
PathTemplate.create("v1/{field=projects/*/instances/*}/actions/{action}");
Truth.assertThat(template.getCanonicalResourceName(knownResources))
.isEqualTo("{field=projects/*/instances/*}");
}

@Test
void testGetCanonicalResourceName_multipleLiteralsWithSimpleBinding() {
Set<String> knownResources = ImmutableSet.of("actions");
PathTemplate template = PathTemplate.create("v1/locations/global/actions/{action}");
Truth.assertThat(template.getCanonicalResourceName(knownResources))
.isEqualTo("locations/global/actions/{action}");
}

@Test
void testGetCanonicalResourceName_multipleLiteralsWithMultipleBindings() {
Set<String> knownResources = ImmutableSet.of("instances", "actions");
PathTemplate template =
PathTemplate.create("v1/locations/global/instances/{instance}/actions/{action}");
Truth.assertThat(template.getCanonicalResourceName(knownResources))
.isEqualTo("locations/global/instances/{instance}/actions/{action}");
}

@Test
void testGetCanonicalResourceName_multipleLiteralsBetweenMultipleBindings() {
Set<String> knownResources = ImmutableSet.of("instances", "actions");
PathTemplate template =
PathTemplate.create("v1/instances/{instance}/locations/global/actions/{action}");
Truth.assertThat(template.getCanonicalResourceName(knownResources))
.isEqualTo("instances/{instance}/locations/global/actions/{action}");
}

@Test
void testGetCanonicalResourceName_nullKnownResources() {
PathTemplate template =
PathTemplate.create("v1/projects/{project}/locations/{location}/widgets/{widget}");
Truth.assertThat(template.getCanonicalResourceName(null)).isEmpty();
}

private static void assertPositionalMatch(Map<String, String> match, String... expected) {
Truth.assertThat(match).isNotNull();
int i = 0;
Expand Down
Loading
Loading