Skip to content

Commit

Permalink
feat: support API versioning in generated protos (#121)
Browse files Browse the repository at this point in the history
If the methods that wind up in a each proto service all have the same
"apiVersion" field in the Discovery document, that value becomes part
of a new "api_version" annotation in the proto service. If the version
numbers for the methods in a service don't match, generation fails.
  • Loading branch information
vchudnov-g authored Apr 8, 2024
1 parent 7f67c1e commit 7ce8f84
Show file tree
Hide file tree
Showing 10 changed files with 6,056 additions and 6 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,8 @@ public static Document from(DiscoveryNode root) {
return thisDocument;
}

// TODO: Combine with parseMethods(). See
// https://github.com/googleapis/disco-to-proto3-converter/issues/123
private static Map<String, List<Method>> parseResources(DiscoveryNode root) {
List<Method> methods = new ArrayList<>();
DiscoveryNode methodsNode = root.getObject("methods");
Expand All @@ -143,6 +145,8 @@ private static Map<String, List<Method>> parseResources(DiscoveryNode root) {
return resources;
}

// TODO: Combine with parseResources(). See
// https://github.com/googleapis/disco-to-proto3-converter/issues/123
private static List<Method> parseMethods(DiscoveryNode root) {
List<Method> methods = new ArrayList<>();
DiscoveryNode methodsNode = root.getObject("methods");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ public static Method from(DiscoveryNode root, Node parent) {
String id = root.getString("id");
String path = root.getString("path");
String flatPath = root.has("flatPath") ? root.getString("flatPath") : path;
String apiVersion = root.getString("apiVersion");

DiscoveryNode parametersNode = root.getObject("parameters");
Map<String, Schema> parameters = new LinkedHashMap<>();
Expand Down Expand Up @@ -105,7 +106,8 @@ public static Method from(DiscoveryNode root, Node parent) {
response,
scopes,
supportsMediaDownload,
supportsMediaUpload);
supportsMediaUpload,
apiVersion);

thisMethod.parent = parent;
if (request != null) {
Expand Down Expand Up @@ -182,6 +184,9 @@ public Node parent() {
/** @return whether or not the method supports media upload. */
public abstract boolean supportsMediaUpload();

/** @return the API version for this method. */
public abstract String apiVersion();

/**
* @return if the method acts on a set of resources whose size may be greater than 1, e.g. List
* methods.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@
import java.util.TreeSet;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.Collectors;

public class DocumentToProtoConverter {

Expand Down Expand Up @@ -738,6 +739,8 @@ private void readResources(Document document) {
GrpcService service =
new GrpcService(grpcServiceName, getServiceDescription(originalGrpcServiceName));
service.getOptions().add(createOption("google.api.default_host", endpoint));
Set<String> methodApiVersions = new HashSet<>();
String serviceApiVersion = "";

Set<String> authScopes = new HashSet<>();

Expand All @@ -748,6 +751,10 @@ private void readResources(Document document) {
authScopes.retainAll(method.scopes());
}

// Record all methods' API versions so we can report them in case of error.
serviceApiVersion = method.apiVersion().trim();
methodApiVersions.add(serviceApiVersion);

// Request
String requestName = getRpcMessageName(method, "request").toUpperCamel();
String methodname = getRpcMethodName(method).toUpperCamel();
Expand Down Expand Up @@ -839,9 +846,16 @@ private void readResources(Document document) {
service.getMethods().add(grpcMethod);
}

service
.getOptions()
.add(createOption("google.api.oauth_scopes", String.join(",", authScopes)));
// Check that all the method API versions match.
if (methodApiVersions.size() != 1) {
throw new InconsistentAPIVersionsException(service.getName(), methodApiVersions);
}

List<Option> serviceOptions = service.getOptions();
serviceOptions.add(createOption("google.api.oauth_scopes", String.join(",", authScopes)));
if (!serviceApiVersion.isEmpty()) {
serviceOptions.add(createOption("google.api.api_version", serviceApiVersion));
}
protoFile.getServices().put(service.getName(), service);
}
}
Expand Down Expand Up @@ -901,8 +915,7 @@ private String sanitizeDescr(String description) {
}

// It is an inefficient way of doing it, but it does not really matter for all possible
// practical
// applications of this converter app.
// practical applications of this converter app.
Matcher m = RELATIVE_LINK.matcher(description);
String sanitizedDescription = description;
while (m.find()) {
Expand All @@ -913,4 +926,19 @@ private String sanitizeDescr(String description) {
return sanitizedDescription.replace(
"{$api_version}", protoFile.getMetadata().getProtoPkgVersion());
}

public class InconsistentAPIVersionsException extends IllegalArgumentException {
public InconsistentAPIVersionsException(String serviceName, Set<String> methodVersions) {
super(
String.format(
"methods for service \"%s\" have inconsistent API version designators: [%s]",
serviceName,
String.join(
" ",
methodVersions
.stream()
.map(version -> String.format("\"%s\"", version))
.collect(Collectors.toList()))));
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertThrows;

import com.google.cloud.discotoproto3converter.proto3.DocumentToProtoConverter;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
Expand Down Expand Up @@ -62,6 +63,154 @@ public void convert() throws IOException {
assertEquals(baselineBody, actualBody);
}

@Test
public void convertVersioned() throws IOException {
// Tests that when all the methods for a single service have identical, non-empty "apiVersion"
// fields in the Discovery file, the proto service gets annotated with the corresponding
// "api_version" annotation.

DiscoToProto3ConverterApp app = new DiscoToProto3ConverterApp();
Path prefix = Paths.get("google", "cloud", "compute", "v1small");
Path discoveryDocPath =
Paths.get("src", "test", "resources", prefix.toString(), "compute.v1small-versioned.json");
Path generatedFilePath =
Paths.get(outputDir.toString(), prefix.toString(), "compute-versioned.proto");

app.convert(
discoveryDocPath.toString(),
null,
generatedFilePath.toString(),
"",
"",
"https://cloud.google.com",
"false",
"true");

String actualBody = readFile(generatedFilePath);

Path baselineFilePath =
Paths.get(
"src", "test", "resources", prefix.toString(), "compute-versioned.proto.baseline");
String baselineBody = readFile(baselineFilePath);
System.out.printf(
"*** @Test:convertVersioned():\n*** Discovery path: %s\n*** Generated file: %s\n*** Baseline file: %s\n",
discoveryDocPath.toAbsolutePath(),
generatedFilePath.toAbsolutePath(),
baselineFilePath.toAbsolutePath());

assertEquals(baselineBody, actualBody);
}

@Test
public void convertVersionedInconsistent() throws IOException {
// Tests that when all the methods in a service have inconsistent non-empty "apiVersion" fields
// in the Discovery file, generation fails.

DiscoToProto3ConverterApp app = new DiscoToProto3ConverterApp();
Path prefix = Paths.get("google", "cloud", "compute", "v1small");
Path discoveryDocPath =
Paths.get(
"src",
"test",
"resources",
prefix.toString(),
"compute.v1small-versioned-inconsistent.json");
Path generatedFilePath =
Paths.get(outputDir.toString(), prefix.toString(), "compute-versioned-inconsistent.proto");

assertThrows(
DocumentToProtoConverter.InconsistentAPIVersionsException.class,
() ->
app.convert(
discoveryDocPath.toString(),
null,
generatedFilePath.toString(),
"",
"",
"https://cloud.google.com",
"true",
"true"));
}

@Test
public void convertVersionedInconsistentEmpty() throws IOException {
// Tests that when all the methods in a service have inconsistent "apiVersion" fields in the
// Discovery file, including empty values, generation fails.

DiscoToProto3ConverterApp app = new DiscoToProto3ConverterApp();
Path prefix = Paths.get("google", "cloud", "compute", "v1small");
Path discoveryDocPath =
Paths.get(
"src",
"test",
"resources",
prefix.toString(),
"compute.v1small-versioned-inconsistent-empty.json");
Path generatedFilePath =
Paths.get(
outputDir.toString(), prefix.toString(), "compute-versioned-inconsistent-empty.proto");

assertThrows(
DocumentToProtoConverter.InconsistentAPIVersionsException.class,
() ->
app.convert(
discoveryDocPath.toString(),
null,
generatedFilePath.toString(),
"",
"",
"https://cloud.google.com",
"true",
"true"));
}

@Test
public void convertVersionedTwoServices() throws IOException {
// Tests that when methods for two services have consistent "apiVersion" fields in the Discovery
// file, the proto services get the correct "api_version" annotation, even if the versions of
// the two services differ.

DiscoToProto3ConverterApp app = new DiscoToProto3ConverterApp();
Path prefix = Paths.get("google", "cloud", "compute", "v1small");
Path discoveryDocPath =
Paths.get(
"src",
"test",
"resources",
prefix.toString(),
"compute.v1small-versioned-two-services.json");
Path generatedFilePath =
Paths.get(outputDir.toString(), prefix.toString(), "compute-versioned-two-services.proto");

app.convert(
discoveryDocPath.toString(),
null,
generatedFilePath.toString(),
"",
"",
"https://cloud.google.com",
"false",
"true");

String actualBody = readFile(generatedFilePath);

Path baselineFilePath =
Paths.get(
"src",
"test",
"resources",
prefix.toString(),
"compute-versioned-two-services.proto.baseline");
String baselineBody = readFile(baselineFilePath);
System.out.printf(
"*** @Test:convertVersionedTwoServices():\n*** Discovery path: %s\n*** Generated file: %s\n*** Baseline file: %s\n",
discoveryDocPath.toAbsolutePath(),
generatedFilePath.toAbsolutePath(),
baselineFilePath.toAbsolutePath());

assertEquals(baselineBody, actualBody);
}

@Test
public void convertWithIgnorelist() throws IOException {
DiscoToProto3ConverterApp app = new DiscoToProto3ConverterApp();
Expand Down
Loading

0 comments on commit 7ce8f84

Please sign in to comment.