Repository: googleapis/java-storage
Pull request #1505 (2022-07-12 20:24:50)

chore: introduce ZOpt
StorageRpc.Option is a difficult and often error prone to use, ZOpt tries to make it easier and safer to work with.

ZOpt defines a set of constants matching those from StorageRpc.Option, except that ZOpt carries the generic type information with it rather than dropping it. This makes usage of these options easier and type safe.



Patch:
diff --git a/google-cloud-storage/src/main/java/com/google/cloud/storage/GrpcStorageImpl.java b/google-cloud-storage/src/main/java/com/google/cloud/storage/GrpcStorageImpl.java
index 455712de..8ec7c431 100644
--- a/google-cloud-storage/src/main/java/com/google/cloud/storage/GrpcStorageImpl.java
+++ b/google-cloud-storage/src/main/java/com/google/cloud/storage/GrpcStorageImpl.java
@@ -137,15 +137,13 @@ final class GrpcStorageImpl extends BaseService<StorageOptions> implements Stora
   public Bucket create(BucketInfo bucketInfo, BucketTargetOption... options) {
     final Map<StorageRpc.Option, ?> optionsMap = StorageImpl.optionMap(options);
     GrpcCallContext grpcCallContext = GrpcRequestMetadataSupport.create(optionsMap);
-    CreateBucketRequest.Builder bucketRequestBuilder =
+    CreateBucketRequest.Builder builder =
         CreateBucketRequest.newBuilder().setBucket(codecs.bucketInfo().encode(bucketInfo));
-    ifNonNull(
-        (String) optionsMap.get(StorageRpc.Option.PREDEFINED_ACL),
-        bucketRequestBuilder::setPredefinedAcl);
-    ifNonNull(
-        (String) optionsMap.get(StorageRpc.Option.PREDEFINED_DEFAULT_OBJECT_ACL),
-        bucketRequestBuilder::setPredefinedDefaultObjectAcl);
-    CreateBucketRequest req = bucketRequestBuilder.build();
+    ZOpt.applyAll(
+        optionsMap,
+        ZOpt.PREDEFINED_ACL.consumeVia(builder::setPredefinedAcl),
+        ZOpt.PREDEFINED_DEFAULT_OBJECT_ACL.consumeVia(builder::setPredefinedDefaultObjectAcl));
+    CreateBucketRequest req = builder.build();
     // TODO(frankyn): Do we care about projection because Apiary uses FULL for projection? Missing
     // projection=full
     return Retrying.run(
@@ -319,14 +317,12 @@ final class GrpcStorageImpl extends BaseService<StorageOptions> implements Stora
   public Bucket get(String bucket, BucketGetOption... options) {
     final Map<StorageRpc.Option, ?> optionsMap = StorageImpl.optionMap(options);
     GrpcCallContext grpcCallContext = GrpcRequestMetadataSupport.create(optionsMap);
-    GetBucketRequest.Builder bucketRequestBuilder = GetBucketRequest.newBuilder().setName(bucket);
-    ifNonNull(
-        (Long) optionsMap.get(StorageRpc.Option.IF_METAGENERATION_MATCH),
-        bucketRequestBuilder::setIfMetagenerationMatch);
-    ifNonNull(
-        (Long) optionsMap.get(StorageRpc.Option.IF_METAGENERATION_NOT_MATCH),
-        bucketRequestBuilder::setIfMetagenerationNotMatch);
-    GetBucketRequest req = bucketRequestBuilder.build();
+    GetBucketRequest.Builder builder = GetBucketRequest.newBuilder().setName(bucket);
+    ZOpt.applyAll(
+        optionsMap,
+        ZOpt.IF_METAGENERATION_MATCH.consumeVia(builder::setIfMetagenerationMatch),
+        ZOpt.IF_METAGENERATION_NOT_MATCH.consumeVia(builder::setIfMetagenerationNotMatch));
+    GetBucketRequest req = builder.build();
     // TODO(frankyn): Do we care about projection because Apiary uses FULL for projection? Missing
     // projection=full
     return Retrying.run(
@@ -350,16 +346,14 @@ final class GrpcStorageImpl extends BaseService<StorageOptions> implements Stora
   public Blob get(BlobId blob, BlobGetOption... options) {
     final Map<StorageRpc.Option, ?> optionsMap = StorageImpl.optionMap(options);
     GrpcCallContext grpcCallContext = GrpcRequestMetadataSupport.create(optionsMap);
-    GetObjectRequest.Builder getObjectRequestBuilder =
+    GetObjectRequest.Builder builder =
         GetObjectRequest.newBuilder().setBucket(blob.getBucket()).setObject(blob.getName());
-    ifNonNull(
-        (Long) optionsMap.get(StorageRpc.Option.IF_METAGENERATION_MATCH),
-        getObjectRequestBuilder::setIfMetagenerationMatch);
-    ifNonNull(
-        (Long) optionsMap.get(StorageRpc.Option.IF_METAGENERATION_NOT_MATCH),
-        getObjectRequestBuilder::setIfMetagenerationNotMatch);
+    ZOpt.applyAll(
+        optionsMap,
+        ZOpt.IF_METAGENERATION_MATCH.consumeVia(builder::setIfMetagenerationMatch),
+        ZOpt.IF_METAGENERATION_NOT_MATCH.consumeVia(builder::setIfMetagenerationNotMatch));
     // TODO(sydmunro) StorageRpc.Option.Fields
-    GetObjectRequest req = getObjectRequestBuilder.build();
+    GetObjectRequest req = builder.build();
     return Retrying.run(
         getOptions(),
         retryAlgorithmManager.getFor(req),
@@ -378,15 +372,14 @@ final class GrpcStorageImpl extends BaseService<StorageOptions> implements Stora
         grpcStorageStub.listBucketsPagedCallable();
     final Map<StorageRpc.Option, ?> optionsMap = StorageImpl.optionMap(options);
     GrpcCallContext grpcCallContext = GrpcRequestMetadataSupport.create(optionsMap);
-    String projectId = (String) optionsMap.get(StorageRpc.Option.PROJECT_ID);
-    if (projectId == null) {
-      projectId = this.getOptions().getProjectId();
-    }
+    String projectId =
+        firstNonNull(ZOpt.PROJECT_ID.get(optionsMap), this.getOptions().getProjectId());
     ListBucketsRequest.Builder builder = ListBucketsRequest.newBuilder().setParent(projectId);
-    ifNonNull(
-        (Long) optionsMap.get(StorageRpc.Option.MAX_RESULTS), Long::intValue, builder::setPageSize);
-    ifNonNull((String) optionsMap.get(StorageRpc.Option.PAGE_TOKEN), builder::setPageToken);
-    ifNonNull((String) optionsMap.get(StorageRpc.Option.PREFIX), builder::setPrefix);
+    ZOpt.applyAll(
+        optionsMap,
+        ZOpt.MAX_RESULTS.mapThenConsumeVia(Long::intValue, builder::setPageSize),
+        ZOpt.PAGE_TOKEN.consumeVia(builder::setPageToken),
+        ZOpt.PREFIX.consumeVia(builder::setPrefix));
     // TODO(sydmunro): StorageRpc.Option.Fields
     ListBucketsPagedResponse call = listBucketsCallable.call(builder.build(), grpcCallContext);
     ListBucketsPage page = call.getPage();
@@ -400,14 +393,14 @@ final class GrpcStorageImpl extends BaseService<StorageOptions> implements Stora
     final Map<StorageRpc.Option, ?> optionsMap = StorageImpl.optionMap(options);
     GrpcCallContext grpcCallContext = GrpcRequestMetadataSupport.create(optionsMap);
     ListObjectsRequest.Builder builder = ListObjectsRequest.newBuilder().setParent(bucket);
-    ifNonNull(
-        (Long) optionsMap.get(StorageRpc.Option.MAX_RESULTS), Long::intValue, builder::setPageSize);
-    ifNonNull((String) optionsMap.get(StorageRpc.Option.PAGE_TOKEN), builder::setPageToken);
-    ifNonNull((String) optionsMap.get(StorageRpc.Option.PREFIX), builder::setPrefix);
-    ifNonNull((String) optionsMap.get(StorageRpc.Option.DELIMITER), builder::setDelimiter);
-    ifNonNull(
-        (String) optionsMap.get(StorageRpc.Option.START_OFF_SET), builder::setLexicographicStart);
-    ifNonNull((String) optionsMap.get(StorageRpc.Option.END_OFF_SET), builder::setLexicographicEnd);
+    ZOpt.applyAll(
+        optionsMap,
+        ZOpt.MAX_RESULTS.mapThenConsumeVia(Long::intValue, builder::setPageSize),
+        ZOpt.PAGE_TOKEN.consumeVia(builder::setPageToken),
+        ZOpt.PREFIX.consumeVia(builder::setPrefix),
+        ZOpt.DELIMITER.consumeVia(builder::setDelimiter),
+        ZOpt.START_OFF_SET.consumeVia(builder::setLexicographicStart),
+        ZOpt.END_OFF_SET.consumeVia(builder::setLexicographicEnd));
     // TODO(sydmunro) StorageRpc.Option.Fields
     ListObjectsPagedResponse call = listObjectsCallable.call(builder.build(), grpcCallContext);
     ListObjectsPage page = call.getPage();
@@ -419,19 +412,14 @@ final class GrpcStorageImpl extends BaseService<StorageOptions> implements Stora
     final Map<StorageRpc.Option, ?> optionsMap = StorageImpl.optionMap(options);
     GrpcCallContext grpcCallContext = GrpcRequestMetadataSupport.create(optionsMap);
     com.google.storage.v2.Bucket bucket = codecs.bucketInfo().encode(bucketInfo);
-    UpdateBucketRequest.Builder bucketRequestBuilder =
-        UpdateBucketRequest.newBuilder().setBucket(bucket);
-    bucketRequestBuilder.setUpdateMask(fieldMaskGenerator(bucket));
-    ifNonNull(
-        (String) optionsMap.get(StorageRpc.Option.PREDEFINED_ACL),
-        bucketRequestBuilder::setPredefinedAcl);
-    ifNonNull(
-        (Long) optionsMap.get(StorageRpc.Option.IF_METAGENERATION_NOT_MATCH),
-        bucketRequestBuilder::setIfMetagenerationNotMatch);
-    ifNonNull(
-        (Long) optionsMap.get(StorageRpc.Option.IF_METAGENERATION_MATCH),
-        bucketRequestBuilder::setIfMetagenerationNotMatch);
-    UpdateBucketRequest req = bucketRequestBuilder.build();
+    UpdateBucketRequest.Builder builder = UpdateBucketRequest.newBuilder().setBucket(bucket);
+    builder.setUpdateMask(fieldMaskGenerator(bucket));
+    ZOpt.applyAll(
+        optionsMap,
+        ZOpt.PREDEFINED_ACL.consumeVia(builder::setPredefinedAcl),
+        ZOpt.IF_METAGENERATION_MATCH.consumeVia(builder::setIfMetagenerationMatch),
+        ZOpt.IF_METAGENERATION_NOT_MATCH.consumeVia(builder::setIfMetagenerationNotMatch));
+    UpdateBucketRequest req = builder.build();
 
     return Retrying.run(
         getOptions(),
@@ -445,25 +433,16 @@ final class GrpcStorageImpl extends BaseService<StorageOptions> implements Stora
     final Map<StorageRpc.Option, ?> optionsMap = StorageImpl.optionMap(options);
     GrpcCallContext grpcCallContext = GrpcRequestMetadataSupport.create(optionsMap);
     Object object = codecs.blobInfo().encode(blobInfo);
-    UpdateObjectRequest.Builder updateRequestBuilder =
-        UpdateObjectRequest.newBuilder().setObject(object);
-    ifNonNull(
-        (Long) optionsMap.get(StorageRpc.Option.IF_GENERATION_MATCH),
-        updateRequestBuilder::setIfGenerationMatch);
-    ifNonNull(
-        (Long) optionsMap.get(StorageRpc.Option.IF_GENERATION_NOT_MATCH),
-        updateRequestBuilder::setIfGenerationNotMatch);
-    ifNonNull(
-        (Long) optionsMap.get(StorageRpc.Option.IF_METAGENERATION_MATCH),
-        updateRequestBuilder::setIfMetagenerationMatch);
-    ifNonNull(
-        (Long) optionsMap.get(StorageRpc.Option.IF_METAGENERATION_NOT_MATCH),
-        updateRequestBuilder::setIfMetagenerationNotMatch);
-    ifNonNull(
-        (String) optionsMap.get(StorageRpc.Option.PREDEFINED_ACL),
-        updateRequestBuilder::setPredefinedAcl);
-    updateRequestBuilder.setUpdateMask(fieldMaskGenerator(object));
-    UpdateObjectRequest req = updateRequestBuilder.build();
+    UpdateObjectRequest.Builder builder = UpdateObjectRequest.newBuilder().setObject(object);
+    ZOpt.applyAll(
+        optionsMap,
+        ZOpt.PREDEFINED_ACL.consumeVia(builder::setPredefinedAcl),
+        ZOpt.IF_METAGENERATION_MATCH.consumeVia(builder::setIfMetagenerationMatch),
+        ZOpt.IF_METAGENERATION_NOT_MATCH.consumeVia(builder::setIfMetagenerationNotMatch),
+        ZOpt.IF_GENERATION_MATCH.consumeVia(builder::setIfGenerationMatch),
+        ZOpt.IF_GENERATION_NOT_MATCH.consumeVia(builder::setIfGenerationNotMatch));
+    builder.setUpdateMask(fieldMaskGenerator(object));
+    UpdateObjectRequest req = builder.build();
     return Retrying.run(
         getOptions(),
         retryAlgorithmManager.getFor(req),
@@ -480,16 +459,14 @@ final class GrpcStorageImpl extends BaseService<StorageOptions> implements Stora
   public boolean delete(String bucket, BucketSourceOption... options) {
     final Map<StorageRpc.Option, ?> optionsMap = StorageImpl.optionMap(options);
     GrpcCallContext grpcCallContext = GrpcRequestMetadataSupport.create(optionsMap);
-    DeleteBucketRequest.Builder bucketRequestBuilder =
+    DeleteBucketRequest.Builder builder =
         DeleteBucketRequest.newBuilder()
             .setName(BucketName.format(this.getOptions().getProjectId(), bucket));
-    ifNonNull(
-        (Long) optionsMap.get(StorageRpc.Option.IF_METAGENERATION_NOT_MATCH),
-        bucketRequestBuilder::setIfMetagenerationNotMatch);
-    ifNonNull(
-        (Long) optionsMap.get(StorageRpc.Option.IF_METAGENERATION_MATCH),
-        bucketRequestBuilder::setIfMetagenerationNotMatch);
-    DeleteBucketRequest req = bucketRequestBuilder.build();
+    ZOpt.applyAll(
+        optionsMap,
+        ZOpt.IF_METAGENERATION_MATCH.consumeVia(builder::setIfMetagenerationMatch),
+        ZOpt.IF_METAGENERATION_NOT_MATCH.consumeVia(builder::setIfMetagenerationNotMatch));
+    DeleteBucketRequest req = builder.build();
     try {
       Retrying.run(
           getOptions(),
@@ -512,22 +489,16 @@ final class GrpcStorageImpl extends BaseService<StorageOptions> implements Stora
   public boolean delete(BlobId blob, BlobSourceOption... options) {
     final Map<StorageRpc.Option, ?> optionsMap = StorageImpl.optionMap(options);
     GrpcCallContext grpcCallContext = GrpcRequestMetadataSupport.create(optionsMap);
-    DeleteObjectRequest.Builder objectRequestBuilder =
+    DeleteObjectRequest.Builder builder =
         DeleteObjectRequest.newBuilder().setBucket(blob.getBucket()).setObject(blob.getName());
-    ifNonNull(
-        (Long) optionsMap.get(StorageRpc.Option.IF_METAGENERATION_NOT_MATCH),
-        objectRequestBuilder::setIfMetagenerationNotMatch);
-    ifNonNull(
-        (Long) optionsMap.get(StorageRpc.Option.IF_METAGENERATION_MATCH),
-        objectRequestBuilder::setIfMetagenerationNotMatch);
-    ifNonNull(
-        (Long) optionsMap.get(StorageRpc.Option.IF_GENERATION_MATCH),
-        objectRequestBuilder::setIfGenerationMatch);
-    ifNonNull(
-        (Long) optionsMap.get(StorageRpc.Option.IF_GENERATION_NOT_MATCH),
-        objectRequestBuilder::setIfGenerationNotMatch);
-    ifNonNull(blob.getGeneration(), objectRequestBuilder::setGeneration);
-    DeleteObjectRequest req = objectRequestBuilder.build();
+    ifNonNull(blob.getGeneration(), builder::setGeneration);
+    ZOpt.applyAll(
+        optionsMap,
+        ZOpt.IF_METAGENERATION_MATCH.consumeVia(builder::setIfMetagenerationMatch),
+        ZOpt.IF_METAGENERATION_NOT_MATCH.consumeVia(builder::setIfMetagenerationNotMatch),
+        ZOpt.IF_GENERATION_MATCH.consumeVia(builder::setIfGenerationMatch),
+        ZOpt.IF_GENERATION_NOT_MATCH.consumeVia(builder::setIfGenerationNotMatch));
+    DeleteObjectRequest req = builder.build();
     try {
       Retrying.run(
           getOptions(),
@@ -886,19 +857,15 @@ final class GrpcStorageImpl extends BaseService<StorageOptions> implements Stora
         grpcStorageStub.listHmacKeysPagedCallable();
     final Map<StorageRpc.Option, ?> optionsMap = StorageImpl.optionMap(options);
     GrpcCallContext grpcCallContext = GrpcRequestMetadataSupport.create(optionsMap);
-    String projectId = (String) optionsMap.get(StorageRpc.Option.PROJECT_ID);
-    if (projectId == null) {
-      projectId = this.getOptions().getProjectId();
-    }
+    String projectId =
+        firstNonNull(ZOpt.PROJECT_ID.get(optionsMap), this.getOptions().getProjectId());
     ListHmacKeysRequest.Builder builder = ListHmacKeysRequest.newBuilder().setProject(projectId);
-    ifNonNull(
-        (String) optionsMap.get(StorageRpc.Option.SERVICE_ACCOUNT_EMAIL),
-        builder::setServiceAccountEmail);
-    ifNonNull(
-        (Long) optionsMap.get(StorageRpc.Option.MAX_RESULTS), Long::intValue, builder::setPageSize);
-    ifNonNull((String) optionsMap.get(StorageRpc.Option.PAGE_TOKEN), builder::setPageToken);
-    ifNonNull(
-        (Boolean) optionsMap.get(StorageRpc.Option.SHOW_DELETED_KEYS), builder::setShowDeletedKeys);
+    ZOpt.applyAll(
+        optionsMap,
+        ZOpt.SERVICE_ACCOUNT_EMAIL.consumeVia(builder::setServiceAccountEmail),
+        ZOpt.MAX_RESULTS.mapThenConsumeVia(Long::intValue, builder::setPageSize),
+        ZOpt.PAGE_TOKEN.consumeVia(builder::setPageToken),
+        ZOpt.SHOW_DELETED_KEYS.consumeVia(builder::setShowDeletedKeys));
     ListHmacKeysPagedResponse call = listHmacKeysCallable.call(builder.build(), grpcCallContext);
     ListHmacKeysPage page = call.getPage();
     return new TransformingPageDecorator<>(page, codecs.hmacKeyMetadata());
@@ -1103,22 +1070,14 @@ final class GrpcStorageImpl extends BaseService<StorageOptions> implements Stora
     if (generation > 0) {
       builder.setGeneration(generation);
     }
-    ifNonNull(
-        (Long) optionsMap.get(StorageRpc.Option.IF_METAGENERATION_MATCH),
-        builder::setIfMetagenerationMatch);
-    ifNonNull(
-        (Long) optionsMap.get(StorageRpc.Option.IF_METAGENERATION_NOT_MATCH),
-        builder::setIfMetagenerationNotMatch);
-    ifNonNull(
-        (Long) optionsMap.get(StorageRpc.Option.IF_GENERATION_MATCH),
-        builder::setIfGenerationMatch);
-    ifNonNull(
-        (Long) optionsMap.get(StorageRpc.Option.IF_GENERATION_NOT_MATCH),
-        builder::setIfGenerationNotMatch);
-    String key = (String) optionsMap.get(StorageRpc.Option.CUSTOMER_SUPPLIED_KEY);
-    if (key != null) {
-      builder.setCommonObjectRequestParams(commonRequestParams(key));
-    }
+    ZOpt.applyAll(
+        optionsMap,
+        ZOpt.IF_METAGENERATION_MATCH.consumeVia(builder::setIfMetagenerationMatch),
+        ZOpt.IF_METAGENERATION_NOT_MATCH.consumeVia(builder::setIfMetagenerationNotMatch),
+        ZOpt.IF_GENERATION_MATCH.consumeVia(builder::setIfGenerationMatch),
+        ZOpt.IF_GENERATION_NOT_MATCH.consumeVia(builder::setIfGenerationNotMatch),
+        ZOpt.CUSTOMER_SUPPLIED_KEY.mapThenConsumeVia(
+            this::commonRequestParams, builder::setCommonObjectRequestParams));
     return builder.build();
   }
 
@@ -1128,7 +1087,7 @@ final class GrpcStorageImpl extends BaseService<StorageOptions> implements Stora
       // GrpcCallContext is immutable, any modification we perform needs to be assigned back to
       // our variable otherwise it will effectively be lost.
       GrpcCallContext ctx = GrpcCallContext.createDefault();
-      String userProject = (String) optionsMap.get(StorageRpc.Option.USER_PROJECT);
+      String userProject = ZOpt.USER_PROJECT.get(optionsMap);
       if (userProject != null) {
         ctx =
             ctx.withExtraHeaders(
@@ -1159,33 +1118,16 @@ final class GrpcStorageImpl extends BaseService<StorageOptions> implements Stora
         WriteObjectRequest.newBuilder().setWriteObjectSpec(specBuilder);
 
     // TODO: Projection: Do we care?
-
-    ifNonNull(
-        (String) optionsMap.get(StorageRpc.Option.PREDEFINED_ACL), specBuilder::setPredefinedAcl);
-    ifNonNull(
-        (Long) optionsMap.get(StorageRpc.Option.IF_METAGENERATION_MATCH),
-        specBuilder::setIfMetagenerationMatch);
-    ifNonNull(
-        (Long) optionsMap.get(StorageRpc.Option.IF_METAGENERATION_NOT_MATCH),
-        specBuilder::setIfMetagenerationNotMatch);
-    ifNonNull(
-        (Long) optionsMap.get(StorageRpc.Option.IF_GENERATION_MATCH),
-        specBuilder::setIfGenerationMatch);
-    ifNonNull(
-        (Long) optionsMap.get(StorageRpc.Option.IF_GENERATION_NOT_MATCH),
-        specBuilder::setIfGenerationNotMatch);
-    String userProject = (String) optionsMap.get(StorageRpc.Option.USER_PROJECT);
-    if (userProject != null) {
-      // TODO: user project
-    }
-    String encryptionKey = (String) optionsMap.get(StorageRpc.Option.CUSTOMER_SUPPLIED_KEY);
-    if (encryptionKey != null) {
-      requestBuilder.setCommonObjectRequestParams(commonRequestParams(encryptionKey));
-    }
-    String key = (String) optionsMap.get(StorageRpc.Option.KMS_KEY_NAME);
-    if (key != null) {
-      objectBuilder.setKmsKey(key);
-    }
+    ZOpt.applyAll(
+        optionsMap,
+        ZOpt.PREDEFINED_ACL.consumeVia(specBuilder::setPredefinedAcl),
+        ZOpt.IF_METAGENERATION_MATCH.consumeVia(specBuilder::setIfMetagenerationMatch),
+        ZOpt.IF_METAGENERATION_NOT_MATCH.consumeVia(specBuilder::setIfMetagenerationNotMatch),
+        ZOpt.IF_GENERATION_MATCH.consumeVia(specBuilder::setIfGenerationMatch),
+        ZOpt.IF_GENERATION_NOT_MATCH.consumeVia(specBuilder::setIfGenerationNotMatch),
+        ZOpt.CUSTOMER_SUPPLIED_KEY.mapThenConsumeVia(
+            this::commonRequestParams, requestBuilder::setCommonObjectRequestParams),
+        ZOpt.KMS_KEY_NAME.consumeVia(objectBuilder::setKmsKey));
 
     return requestBuilder;
   }
diff --git a/google-cloud-storage/src/main/java/com/google/cloud/storage/ZOpt.java b/google-cloud-storage/src/main/java/com/google/cloud/storage/ZOpt.java
new file mode 100644
index 00000000..6c77c2dc
--- /dev/null
+++ b/google-cloud-storage/src/main/java/com/google/cloud/storage/ZOpt.java
@@ -0,0 +1,181 @@
+/*
+ * Copyright 2022 Google LLC
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *       http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.google.cloud.storage;
+
+import com.google.cloud.storage.spi.v1.StorageRpc;
+import java.util.Map;
+import java.util.function.Consumer;
+import java.util.function.Function;
+import org.checkerframework.checker.nullness.qual.NonNull;
+import org.checkerframework.checker.nullness.qual.Nullable;
+
+/**
+ * A new helper abstraction which makes working with {@link StorageRpc.Option}s more type safe, and
+ * null safe. When working with a {@code Map<StorageRpc.Option, ?>} we loose the independent type
+ * information for each key. The type conversion methods on StorageRpc.Option are package-private,
+ * and we DO NOT want to make them public. Which leads to this new class, which essentially creates
+ * a mapping between a java type and a specific {@code StorageRpc.Option}.
+ *
+ * <p>We are defining each Option as a constant rather than an enum, because enum's can't have
+ * generic class parameters. This shouldn't pose an issue:
+ *
+ * <ol>
+ *   <li>This class is package-private
+ *   <li>This class is sealed, and can only be constructed from within it's definition
+ *   <li>This class is strictly used for mapping, never a property in a model that could be
+ *       serialized
+ * </ol>
+ */
+final class ZOpt<T> {
+
+  static final ZOpt<String> PREDEFINED_ACL = new ZOpt<>(StorageRpc.Option.PREDEFINED_ACL);
+  static final ZOpt<String> PREDEFINED_DEFAULT_OBJECT_ACL =
+      new ZOpt<>(StorageRpc.Option.PREDEFINED_DEFAULT_OBJECT_ACL);
+  static final ZOpt<Long> IF_METAGENERATION_MATCH =
+      new ZOpt<>(StorageRpc.Option.IF_METAGENERATION_MATCH);
+  static final ZOpt<Long> IF_METAGENERATION_NOT_MATCH =
+      new ZOpt<>(StorageRpc.Option.IF_METAGENERATION_NOT_MATCH);
+  static final ZOpt<Long> IF_GENERATION_MATCH = new ZOpt<>(StorageRpc.Option.IF_GENERATION_MATCH);
+  static final ZOpt<Long> IF_GENERATION_NOT_MATCH =
+      new ZOpt<>(StorageRpc.Option.IF_GENERATION_NOT_MATCH);
+  static final ZOpt<Long> IF_SOURCE_METAGENERATION_MATCH =
+      new ZOpt<>(StorageRpc.Option.IF_SOURCE_METAGENERATION_MATCH);
+  static final ZOpt<Long> IF_SOURCE_METAGENERATION_NOT_MATCH =
+      new ZOpt<>(StorageRpc.Option.IF_SOURCE_METAGENERATION_NOT_MATCH);
+  static final ZOpt<Long> IF_SOURCE_GENERATION_MATCH =
+      new ZOpt<>(StorageRpc.Option.IF_SOURCE_GENERATION_MATCH);
+  static final ZOpt<Long> IF_SOURCE_GENERATION_NOT_MATCH =
+      new ZOpt<>(StorageRpc.Option.IF_SOURCE_GENERATION_NOT_MATCH);
+  static final ZOpt<Boolean> IF_DISABLE_GZIP_CONTENT =
+      new ZOpt<>(StorageRpc.Option.IF_DISABLE_GZIP_CONTENT);
+  static final ZOpt<String> PREFIX = new ZOpt<>(StorageRpc.Option.PREFIX);
+  static final ZOpt<String> PROJECT_ID = new ZOpt<>(StorageRpc.Option.PROJECT_ID);
+  static final ZOpt<String> PROJECTION = new ZOpt<>(StorageRpc.Option.PROJECTION);
+  static final ZOpt<Long> MAX_RESULTS = new ZOpt<>(StorageRpc.Option.MAX_RESULTS);
+  static final ZOpt<String> PAGE_TOKEN = new ZOpt<>(StorageRpc.Option.PAGE_TOKEN);
+  static final ZOpt<String> DELIMITER = new ZOpt<>(StorageRpc.Option.DELIMITER);
+  static final ZOpt<String> START_OFF_SET = new ZOpt<>(StorageRpc.Option.START_OFF_SET);
+  static final ZOpt<String> END_OFF_SET = new ZOpt<>(StorageRpc.Option.END_OFF_SET);
+  static final ZOpt<Boolean> VERSIONS = new ZOpt<>(StorageRpc.Option.VERSIONS);
+  static final ZOpt<String> FIELDS = new ZOpt<>(StorageRpc.Option.FIELDS);
+  static final ZOpt<String> CUSTOMER_SUPPLIED_KEY =
+      new ZOpt<>(StorageRpc.Option.CUSTOMER_SUPPLIED_KEY);
+  static final ZOpt<String> USER_PROJECT = new ZOpt<>(StorageRpc.Option.USER_PROJECT);
+  static final ZOpt<String> KMS_KEY_NAME = new ZOpt<>(StorageRpc.Option.KMS_KEY_NAME);
+  static final ZOpt<String> SERVICE_ACCOUNT_EMAIL =
+      new ZOpt<>(StorageRpc.Option.SERVICE_ACCOUNT_EMAIL);
+  static final ZOpt<Boolean> SHOW_DELETED_KEYS = new ZOpt<>(StorageRpc.Option.SHOW_DELETED_KEYS);
+  static final ZOpt<Long> REQUESTED_POLICY_VERSION =
+      new ZOpt<>(StorageRpc.Option.REQUESTED_POLICY_VERSION);
+  static final ZOpt<String> DETECT_CONTENT_TYPE = new ZOpt<>(StorageRpc.Option.DETECT_CONTENT_TYPE);
+  static final ZOpt<Boolean> RETURN_RAW_INPUT_STREAM =
+      new ZOpt<>(StorageRpc.Option.RETURN_RAW_INPUT_STREAM);
+
+  private final StorageRpc.Option key;
+
+  private ZOpt(StorageRpc.Option key) {
+    this.key = key;
+  }
+
+  StorageRpc.Option getKey() {
+    return key;
+  }
+
+  /**
+   * This method is only ever called from package private scope, as such the Map of option values
+   * has prior type checked values which are later erased. Here we suppress the unchecked warning
+   * since it's guarded ealier in the conversion.
+   */
+  @SuppressWarnings("unchecked")
+  T cast(Object o) {
+    return (T) o;
+  }
+
+  /**
+   * Attempt to get the typed value from {@code optionValues}. If the key is not present in the
+   * provided {@code optionValues} null will be returned.
+   */
+  @Nullable
+  T get(@NonNull Map<StorageRpc.Option, ?> optionValues) {
+    Object o = optionValues.get(key);
+    return o == null ? null : cast(o);
+  }
+
+  /**
+   * Create a new {@link ZOptConsumer} bound to the provided {@code consumer}
+   *
+   * @see ZOptConsumer#apply(Map)
+   */
+  ZOptConsumer<T, T> consumeVia(Consumer<@NonNull T> consumer) {
+    return new ZOptConsumer<>(this, Function.identity(), consumer);
+  }
+
+  /**
+   * Create a new {@link ZOptConsumer} which will apply {@code mapper} before consumption.
+   *
+   * @see ZOptConsumer#apply(Map)
+   */
+  <X> ZOptConsumer<T, X> mapThenConsumeVia(Function<T, X> mapper, Consumer<@NonNull X> consumer) {
+    return new ZOptConsumer<>(this, mapper, consumer);
+  }
+
+  /**
+   * For each consumer in {@code consumers} invoke {@link ZOptConsumer#apply(Map)} with {@code
+   * optionValues}
+   */
+  static void applyAll(Map<StorageRpc.Option, ?> optionValues, ZOptConsumer<?, ?>... consumers) {
+    for (ZOptConsumer<?, ?> consumer : consumers) {
+      consumer.apply(optionValues);
+    }
+  }
+
+  static class ZOptConsumer<T, X> {
+    private final ZOpt<T> opt;
+    private final Function<@NonNull T, @Nullable X> mapper;
+    private final Consumer<@Nullable X> consumer;
+
+    private ZOptConsumer(
+        ZOpt<T> opt, Function<@NonNull T, @Nullable X> mapper, Consumer<@NonNull X> consumer) {
+      this.opt = opt;
+      this.mapper = mapper;
+      this.consumer = consumer;
+    }
+
+    /**
+     * Apply this consumer to the provided {@code optionValues}.
+     *
+     * <p>A null value will never be passed to the {@code consumer}.
+     *
+     * <ul>
+     *   <li>If the provided {@code optionValues} does not contain a value related to {@code
+     *       this.opt.key}, the method will terminate.
+     *   <li>If the provided {@code optionValues} does contains a value, it will be sent through
+     *       {@code this.mapper} if the result is non-null it will be passed to {@code
+     *       this.consumer} otherwise the method will terminate.
+     * </ul>
+     */
+    void apply(Map<StorageRpc.Option, ?> optionValues) {
+      T t = opt.get(optionValues);
+      if (t != null) {
+        X x = mapper.apply(t);
+        if (x != null) {
+          consumer.accept(x);
+        }
+      }
+    }
+  }
+}


Test patch:
diff --git a/google-cloud-storage/src/test/java/com/google/cloud/storage/ZOptTest.java b/google-cloud-storage/src/test/java/com/google/cloud/storage/ZOptTest.java
new file mode 100644
index 00000000..7508ad38
--- /dev/null
+++ b/google-cloud-storage/src/test/java/com/google/cloud/storage/ZOptTest.java
@@ -0,0 +1,137 @@
+/*
+ * Copyright 2022 Google LLC
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *       http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.google.cloud.storage;
+
+import static com.google.common.truth.Truth.assertThat;
+import static org.junit.Assert.fail;
+
+import com.google.cloud.storage.spi.v1.StorageRpc;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableMap;
+import java.lang.reflect.Field;
+import java.lang.reflect.Modifier;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.concurrent.atomic.AtomicBoolean;
+import java.util.concurrent.atomic.AtomicLong;
+import java.util.concurrent.atomic.AtomicReference;
+import org.junit.Test;
+
+public final class ZOptTest {
+
+  @Test
+  public void happyPath() {
+    AtomicReference<String> ref1 = new AtomicReference<>();
+    AtomicLong ref2 = new AtomicLong(Long.MAX_VALUE);
+
+    ImmutableMap<StorageRpc.Option, ?> map =
+        ImmutableMap.of(StorageRpc.Option.PREDEFINED_ACL, "acl");
+    ZOpt.PREDEFINED_ACL.consumeVia(ref1::set).apply(map);
+    ZOpt.IF_GENERATION_MATCH.consumeVia(ref2::set).apply(map);
+
+    assertThat(ref1.get()).isEqualTo("acl");
+    assertThat(ref2.get()).isEqualTo(Long.MAX_VALUE);
+  }
+
+  @Test
+  public void applyAll() {
+    AtomicReference<String> ref1 = new AtomicReference<>();
+    AtomicLong ref2 = new AtomicLong(Long.MAX_VALUE);
+
+    ImmutableMap<StorageRpc.Option, ?> map =
+        ImmutableMap.of(StorageRpc.Option.PREDEFINED_ACL, "acl");
+    ZOpt.applyAll(
+        map,
+        ZOpt.PREDEFINED_ACL.consumeVia(ref1::set),
+        ZOpt.IF_GENERATION_MATCH.consumeVia(ref2::set));
+
+    assertThat(ref1.get()).isEqualTo("acl");
+    assertThat(ref2.get()).isEqualTo(Long.MAX_VALUE);
+  }
+
+  @Test
+  public void aNullValueShouldNotBeProvidedToTheConsumer() {
+    ZOpt.PREDEFINED_ACL
+        .consumeVia((v) -> fail("null should not be provided"))
+        .apply(Collections.emptyMap());
+  }
+
+  @Test
+  public void ifMapperProducesNullItShouldNotBeProvidedToTheConsumer() {
+    ImmutableMap<StorageRpc.Option, ?> map =
+        ImmutableMap.of(StorageRpc.Option.PREDEFINED_ACL, "acl");
+    ZOpt.PREDEFINED_ACL
+        .mapThenConsumeVia((t) -> null, (v) -> fail("null should not be provided"))
+        .apply(map);
+  }
+
+  @Test
+  public void ensureEachTypeWorks() {
+    AtomicReference<String> ref1 = new AtomicReference<>();
+    AtomicLong ref2 = new AtomicLong(Long.MAX_VALUE);
+    AtomicBoolean ref3 = new AtomicBoolean(false);
+
+    ImmutableMap<StorageRpc.Option, ?> map =
+        ImmutableMap.of(
+            StorageRpc.Option.PREDEFINED_ACL,
+            "acl",
+            StorageRpc.Option.IF_GENERATION_MATCH,
+            0L,
+            StorageRpc.Option.IF_DISABLE_GZIP_CONTENT,
+            true);
+    ZOpt.applyAll(
+        map,
+        ZOpt.PREDEFINED_ACL.consumeVia(ref1::set),
+        ZOpt.IF_GENERATION_MATCH.consumeVia(ref2::set),
+        ZOpt.IF_DISABLE_GZIP_CONTENT.consumeVia(ref3::set));
+
+    assertThat(ref1.get()).isEqualTo("acl");
+    assertThat(ref2.get()).isEqualTo(0L);
+    assertThat(ref3.get()).isTrue();
+  }
+
+  @Test
+  public void validateCoverageOfAllStorageRpcOptions() {
+    Comparator<StorageRpc.Option> comp = Comparator.comparingInt(Enum::ordinal);
+
+    ImmutableList<StorageRpc.Option> expected =
+        Arrays.stream(StorageRpc.Option.values())
+            .sorted(comp)
+            .collect(ImmutableList.toImmutableList());
+
+    ImmutableList<StorageRpc.Option> actual =
+        Arrays.stream(ZOpt.class.getDeclaredFields())
+            .filter(f -> Modifier.isStatic(f.getModifiers()))
+            .filter(f -> !Modifier.isPrivate(f.getModifiers()))
+            .filter(f -> f.getType().equals(ZOpt.class))
+            .map(ZOptTest::unsafeGet)
+            .map(ZOpt::getKey)
+            .sorted(comp)
+            .collect(ImmutableList.toImmutableList());
+
+    assertThat(actual).containsExactlyElementsIn(expected);
+  }
+
+  private static ZOpt<?> unsafeGet(Field f) {
+    try {
+      return (ZOpt<?>) f.get(null);
+    } catch (IllegalAccessException e) {
+      throw new RuntimeException("error attempting to access field: " + f, e);
+    }
+  }
+}