Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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.
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;
} 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
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);
}
}
}
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/*}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Can we also add a comment line saying what would be the output given an exemplary template?

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

// 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.
public String getCanonicalResourceName(Set<String> knownResources) {
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) {
String value = seg.value();
if (value.matches("^v\\d+[a-zA-Z0-9]*$")) {
startIndex = i + 1;
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Is this if statement guaranteed to end up setting the start index?

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.

Yes, if there are not versions found, the start index would be 0.

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("=*}", "}");
}

/**
* 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");
}

@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