diff --git a/BUILD.bazel b/BUILD.bazel index 8ebb74dc86..0b8f9abb03 100644 --- a/BUILD.bazel +++ b/BUILD.bazel @@ -192,6 +192,14 @@ GOLDEN_UPDATING_UNIT_TESTS = [ "com.google.api.generator.gapic.composer.grpc.ServiceSettingsClassComposerTest", "com.google.api.generator.gapic.composer.grpc.ServiceStubClassComposerTest", "com.google.api.generator.gapic.composer.grpc.ServiceStubSettingsClassComposerTest", + "com.google.api.generator.gapic.composer.grpcrest.GrpcServiceCallableFactoryClassComposerTest", + "com.google.api.generator.gapic.composer.grpcrest.GrpcServiceStubClassComposerTest", + "com.google.api.generator.gapic.composer.grpcrest.HttpJsonServiceCallableFactoryClassComposerTest", + "com.google.api.generator.gapic.composer.grpcrest.HttpJsonServiceClientTestClassComposerTest", + "com.google.api.generator.gapic.composer.grpcrest.HttpJsonServiceStubClassComposerTest", + "com.google.api.generator.gapic.composer.grpcrest.ServiceClientClassComposerTest", + "com.google.api.generator.gapic.composer.grpcrest.ServiceClientTestClassComposerTest", + "com.google.api.generator.gapic.composer.grpcrest.ServiceSettingsClassComposerTest", "com.google.api.generator.gapic.composer.resourcename.ResourceNameHelperClassComposerTest", "com.google.api.generator.gapic.composer.rest.HttpJsonServiceCallableFactoryClassComposerTest", "com.google.api.generator.gapic.composer.rest.HttpJsonServiceStubClassComposerTest", diff --git a/src/main/java/com/google/api/generator/gapic/composer/common/AbstractServiceStubSettingsClassComposer.java b/src/main/java/com/google/api/generator/gapic/composer/common/AbstractServiceStubSettingsClassComposer.java index a7d78287fa..bfedd5e190 100644 --- a/src/main/java/com/google/api/generator/gapic/composer/common/AbstractServiceStubSettingsClassComposer.java +++ b/src/main/java/com/google/api/generator/gapic/composer/common/AbstractServiceStubSettingsClassComposer.java @@ -392,8 +392,7 @@ private static List createClassHeaderComments( .filter(m -> m.stream() == Stream.NONE && !m.hasLro() && !m.isPaged()) .findFirst() .orElse(service.methods().get(0))); - Optional methodNameOpt = - methodOpt.isPresent() ? Optional.of(methodOpt.get().name()) : Optional.empty(); + Optional methodNameOpt = methodOpt.map(Method::name); Optional sampleCode = SettingsSampleComposer.composeSettingsSample( methodNameOpt, ClassNames.getServiceSettingsClassName(service), classType); @@ -437,7 +436,7 @@ private static Map createMethodSettingsClassMemberVarExprs !Objects.isNull(serviceConfig) && serviceConfig.hasBatchingSetting(service, method); TypeNode settingsType = getCallSettingsType(method, typeStore, hasBatchingSettings, isNestedClass); - String varName = JavaStyle.toLowerCamelCase(String.format("%sSettings", method.name())); + String varName = String.format("%sSettings", JavaStyle.toLowerCamelCase(method.name())); if (method.isDeprecated()) { deprecatedSettingVarNames.add(varName); } diff --git a/src/main/java/com/google/api/generator/gapic/composer/samplecode/ServiceClientHeaderSampleComposer.java b/src/main/java/com/google/api/generator/gapic/composer/samplecode/ServiceClientHeaderSampleComposer.java index e3673dc7a9..7dea0cca50 100644 --- a/src/main/java/com/google/api/generator/gapic/composer/samplecode/ServiceClientHeaderSampleComposer.java +++ b/src/main/java/com/google/api/generator/gapic/composer/samplecode/ServiceClientHeaderSampleComposer.java @@ -51,6 +51,10 @@ public static Sample composeClassHeaderSample( TypeNode clientType, Map resourceNames, Map messageTypes) { + if (service.methods().isEmpty()) { + return ServiceClientMethodSampleComposer.composeEmptyServiceSample(clientType); + } + // Use the first pure unary RPC method's sample code as showcase, if no such method exists, use // the first method in the service's methods list. Method method = @@ -58,6 +62,7 @@ public static Sample composeClassHeaderSample( .filter(m -> m.stream() == Method.Stream.NONE && !m.hasLro() && !m.isPaged()) .findFirst() .orElse(service.methods().get(0)); + if (method.stream() == Method.Stream.NONE) { if (method.methodSignatures().isEmpty()) { return ServiceClientMethodSampleComposer.composeCanonicalSample( diff --git a/src/main/java/com/google/api/generator/gapic/composer/samplecode/ServiceClientMethodSampleComposer.java b/src/main/java/com/google/api/generator/gapic/composer/samplecode/ServiceClientMethodSampleComposer.java index 167c8afa4d..9fa91574f6 100644 --- a/src/main/java/com/google/api/generator/gapic/composer/samplecode/ServiceClientMethodSampleComposer.java +++ b/src/main/java/com/google/api/generator/gapic/composer/samplecode/ServiceClientMethodSampleComposer.java @@ -42,6 +42,36 @@ import java.util.stream.Collectors; public class ServiceClientMethodSampleComposer { + // Creates an example for an empty service (no API methods), which is a corner case but can + // happen. Generated example will only show how to instantiate the client class but will not call + // any API methods (because there are no API methods). + public static Sample composeEmptyServiceSample(TypeNode clientType) { + VariableExpr clientVarExpr = + VariableExpr.withVariable( + Variable.builder() + .setName(JavaStyle.toLowerCamelCase(clientType.reference().name())) + .setType(clientType) + .build()); + + List bodyStatements = new ArrayList<>(); + + RegionTag regionTag = + RegionTag.builder() + .setServiceName(clientVarExpr.variable().identifier().name()) + .setRpcName("emtpy") + .build(); + + List body = + Arrays.asList( + TryCatchStatement.builder() + .setTryResourceExpr( + SampleComposerUtil.assignClientVariableWithCreateMethodExpr(clientVarExpr)) + .setTryBody(bodyStatements) + .setIsSampleCode(true) + .build()); + return Sample.builder().setBody(body).setRegionTag(regionTag).setIsCanonical(true).build(); + } + public static Sample composeCanonicalSample( Method method, TypeNode clientType, diff --git a/src/main/java/com/google/api/generator/gapic/utils/JavaStyle.java b/src/main/java/com/google/api/generator/gapic/utils/JavaStyle.java index 90efae459e..d99777bcd2 100644 --- a/src/main/java/com/google/api/generator/gapic/utils/JavaStyle.java +++ b/src/main/java/com/google/api/generator/gapic/utils/JavaStyle.java @@ -14,6 +14,7 @@ package com.google.api.generator.gapic.utils; +import com.google.api.generator.engine.lexicon.Keyword; import com.google.common.base.CaseFormat; import com.google.common.base.Strings; import java.util.stream.IntStream; @@ -32,8 +33,13 @@ public static String toLowerCamelCase(String s) { s = CaseFormat.LOWER_UNDERSCORE.to(CaseFormat.UPPER_CAMEL, s); } - return capitalizeLettersAfterDigits( - String.format("%s%s", s.substring(0, 1).toLowerCase(), s.substring(1))); + String name = + capitalizeLettersAfterDigits( + String.format("%s%s", s.substring(0, 1).toLowerCase(), s.substring(1))); + + // Some APIs use legit java keywords as method names. Both protobuf and gGRPC add an underscore + // in generated stubs to resolve name conflict, so we need to do the same. + return Keyword.isKeyword(name) ? name + '_' : name; } public static String toUpperCamelCase(String s) { diff --git a/src/test/java/com/google/api/generator/gapic/composer/grpcrest/ServiceClientClassComposerTest.java b/src/test/java/com/google/api/generator/gapic/composer/grpcrest/ServiceClientClassComposerTest.java index ff499acd8b..0be072badd 100644 --- a/src/test/java/com/google/api/generator/gapic/composer/grpcrest/ServiceClientClassComposerTest.java +++ b/src/test/java/com/google/api/generator/gapic/composer/grpcrest/ServiceClientClassComposerTest.java @@ -37,4 +37,17 @@ public void generateServiceClasses() { Path goldenFilePath = Paths.get(Utils.getGoldenDir(this.getClass()), "EchoClient.golden"); Assert.assertCodeEquals(goldenFilePath, visitor.write()); } + + @Test + public void generateServiceClassesEmpty() { + GapicContext context = GrpcRestTestProtoLoader.instance().parseShowcaseEcho(); + Service echoProtoService = context.services().get(1); + GapicClass clazz = ServiceClientClassComposer.instance().generate(context, echoProtoService); + + JavaWriterVisitor visitor = new JavaWriterVisitor(); + clazz.classDefinition().accept(visitor); + Utils.saveCodegenToFile(this.getClass(), "EchoEmpty.golden", visitor.write()); + Path goldenFilePath = Paths.get(Utils.getGoldenDir(this.getClass()), "EchoEmpty.golden"); + Assert.assertCodeEquals(goldenFilePath, visitor.write()); + } } diff --git a/src/test/java/com/google/api/generator/gapic/composer/grpcrest/goldens/EchoEmpty.golden b/src/test/java/com/google/api/generator/gapic/composer/grpcrest/goldens/EchoEmpty.golden new file mode 100644 index 0000000000..95547b9cee --- /dev/null +++ b/src/test/java/com/google/api/generator/gapic/composer/grpcrest/goldens/EchoEmpty.golden @@ -0,0 +1,167 @@ +package com.google.showcase.grpcrest.v1beta1; + +import com.google.api.core.BetaApi; +import com.google.api.gax.core.BackgroundResource; +import com.google.showcase.grpcrest.v1beta1.stub.EchoEmpyStub; +import com.google.showcase.grpcrest.v1beta1.stub.EchoEmpyStubSettings; +import java.io.IOException; +import java.util.concurrent.TimeUnit; +import javax.annotation.Generated; + +// AUTO-GENERATED DOCUMENTATION AND CLASS. +/** + * This class provides the ability to make remote calls to the backing service through method calls + * that map to API methods. Sample code to get started: + * + *
{@code
+ * // This snippet has been automatically generated for illustrative purposes only.
+ * // It may require modifications to work in your environment.
+ * try (EchoEmpyClient echoEmpyClient = EchoEmpyClient.create()) {}
+ * }
+ * + *

Note: close() needs to be called on the EchoEmpyClient object to clean up resources such as + * threads. In the example above, try-with-resources is used, which automatically calls close(). + * + *

The surface of this class includes several types of Java methods for each of the API's + * methods: + * + *

    + *
  1. A "flattened" method. With this type of method, the fields of the request type have been + * converted into function parameters. It may be the case that not all fields are available as + * parameters, and not every API method will have a flattened method entry point. + *
  2. A "request object" method. This type of method only takes one parameter, a request object, + * which must be constructed before the call. Not every API method will have a request object + * method. + *
  3. A "callable" method. This type of method takes no parameters and returns an immutable API + * callable object, which can be used to initiate calls to the service. + *
+ * + *

See the individual methods for example code. + * + *

Many parameters require resource names to be formatted in a particular way. To assist with + * these names, this class includes a format method for each type of name, and additionally a parse + * method to extract the individual identifiers contained within names that are returned. + * + *

This class can be customized by passing in a custom instance of EchoEmpySettings to create(). + * For example: + * + *

To customize credentials: + * + *

{@code
+ * // This snippet has been automatically generated for illustrative purposes only.
+ * // It may require modifications to work in your environment.
+ * EchoEmpySettings echoEmpySettings =
+ *     EchoEmpySettings.newBuilder()
+ *         .setCredentialsProvider(FixedCredentialsProvider.create(myCredentials))
+ *         .build();
+ * EchoEmpyClient echoEmpyClient = EchoEmpyClient.create(echoEmpySettings);
+ * }
+ * + *

To customize the endpoint: + * + *

{@code
+ * // This snippet has been automatically generated for illustrative purposes only.
+ * // It may require modifications to work in your environment.
+ * EchoEmpySettings echoEmpySettings =
+ *     EchoEmpySettings.newBuilder().setEndpoint(myEndpoint).build();
+ * EchoEmpyClient echoEmpyClient = EchoEmpyClient.create(echoEmpySettings);
+ * }
+ * + *

To use REST (HTTP1.1/JSON) transport (instead of gRPC) for sending an receiving requests over + * the wire: + * + *

{@code
+ * // This snippet has been automatically generated for illustrative purposes only.
+ * // It may require modifications to work in your environment.
+ * EchoEmpySettings echoEmpySettings =
+ *     EchoEmpySettings.newBuilder()
+ *         .setTransportChannelProvider(
+ *             EchoEmpySettings.defaultHttpJsonTransportProviderBuilder().build())
+ *         .build();
+ * EchoEmpyClient echoEmpyClient = EchoEmpyClient.create(echoEmpySettings);
+ * }
+ * + *

Please refer to the GitHub repository's samples for more quickstart code snippets. + */ +@BetaApi +@Generated("by gapic-generator-java") +public class EchoEmpyClient implements BackgroundResource { + private final EchoEmpySettings settings; + private final EchoEmpyStub stub; + + /** Constructs an instance of EchoEmpyClient with default settings. */ + public static final EchoEmpyClient create() throws IOException { + return create(EchoEmpySettings.newBuilder().build()); + } + + /** + * Constructs an instance of EchoEmpyClient, using the given settings. The channels are created + * based on the settings passed in, or defaults for any settings that are not set. + */ + public static final EchoEmpyClient create(EchoEmpySettings settings) throws IOException { + return new EchoEmpyClient(settings); + } + + /** + * Constructs an instance of EchoEmpyClient, using the given stub for making calls. This is for + * advanced usage - prefer using create(EchoEmpySettings). + */ + @BetaApi("A restructuring of stub classes is planned, so this may break in the future") + public static final EchoEmpyClient create(EchoEmpyStub stub) { + return new EchoEmpyClient(stub); + } + + /** + * Constructs an instance of EchoEmpyClient, using the given settings. This is protected so that + * it is easy to make a subclass, but otherwise, the static factory methods should be preferred. + */ + protected EchoEmpyClient(EchoEmpySettings settings) throws IOException { + this.settings = settings; + this.stub = ((EchoEmpyStubSettings) settings.getStubSettings()).createStub(); + } + + @BetaApi("A restructuring of stub classes is planned, so this may break in the future") + protected EchoEmpyClient(EchoEmpyStub stub) { + this.settings = null; + this.stub = stub; + } + + public final EchoEmpySettings getSettings() { + return settings; + } + + @BetaApi("A restructuring of stub classes is planned, so this may break in the future") + public EchoEmpyStub getStub() { + return stub; + } + + @Override + public final void close() { + stub.close(); + } + + @Override + public void shutdown() { + stub.shutdown(); + } + + @Override + public boolean isShutdown() { + return stub.isShutdown(); + } + + @Override + public boolean isTerminated() { + return stub.isTerminated(); + } + + @Override + public void shutdownNow() { + stub.shutdownNow(); + } + + @Override + public boolean awaitTermination(long duration, TimeUnit unit) throws InterruptedException { + return stub.awaitTermination(duration, unit); + } +} diff --git a/src/test/java/com/google/api/generator/gapic/utils/JavaStyleTest.java b/src/test/java/com/google/api/generator/gapic/utils/JavaStyleTest.java index 3cf7d647cc..fec084555c 100644 --- a/src/test/java/com/google/api/generator/gapic/utils/JavaStyleTest.java +++ b/src/test/java/com/google/api/generator/gapic/utils/JavaStyleTest.java @@ -111,4 +111,14 @@ public void acronyms() { assertEquals("iamHttpXmlDog", JavaStyle.toLowerCamelCase(value)); assertEquals("IamHttpXmlDog", JavaStyle.toUpperCamelCase(value)); } + + @Test + public void keyword() { + String value = "import"; + assertEquals("import_", JavaStyle.toLowerCamelCase(value)); + assertEquals("Import", JavaStyle.toUpperCamelCase(value)); + value = "IMPORT_"; + assertEquals("import_", JavaStyle.toLowerCamelCase(value)); + assertEquals("Import", JavaStyle.toUpperCamelCase(value)); + } } diff --git a/src/test/proto/echo_grpcrest.proto b/src/test/proto/echo_grpcrest.proto index 7568b8bc04..1c71c96fbc 100644 --- a/src/test/proto/echo_grpcrest.proto +++ b/src/test/proto/echo_grpcrest.proto @@ -122,6 +122,11 @@ service Echo { } } +// Generator should not fail when encounter a service without methods +service EchoEmpy { + option (google.api.default_host) = "localhost:7469"; +} + // A severity enum used to test enum capabilities in GAPIC surfaces enum Severity { UNNECESSARY = 0;