Repository: googleapis/java-storage
Pull request #1385 (2022-05-12 00:57:51)

feat: bucket metadata
Thank you for opening a Pull Request! Before submitting your PR, there are a few things you can do to make sure it goes smoothly:
- [ ] Make sure to open an issue as a [bug/issue](https://github.com/googleapis/java-storage/issues/new/choose) before writing your code!  That way we can discuss the change, evaluate designs, and agree on the general idea
- [ ] Ensure the tests and linter pass
- [ ] Code coverage does not decrease (if any source code was changed)
- [ ] Appropriate docs were updated (if necessary)

Fixes #<issue_number_goes_here> ☕️

If you write sample code, please follow the [samples format](
https://github.com/GoogleCloudPlatform/java-docs-samples/blob/main/SAMPLE_FORMAT.md).



Patch:
diff --git a/google-cloud-storage/src/main/java/com/google/cloud/storage/ApiaryConversions.java b/google-cloud-storage/src/main/java/com/google/cloud/storage/ApiaryConversions.java
index 4f7b4a7e..fe267a5a 100644
--- a/google-cloud-storage/src/main/java/com/google/cloud/storage/ApiaryConversions.java
+++ b/google-cloud-storage/src/main/java/com/google/cloud/storage/ApiaryConversions.java
@@ -71,7 +71,6 @@ import java.util.HashSet;
 import java.util.List;
 import java.util.Map;
 import java.util.Set;
-import java.util.function.Function;
 
 @InternalApi
 final class ApiaryConversions {
@@ -183,7 +182,7 @@ final class ApiaryConversions {
 
   private StorageObject blobInfoEncode(BlobInfo from) {
     StorageObject to = blobIdEncode(from.getBlobId());
-    ifNonNull(from.getAcl(), toImmutableListOf(objectAcl()::encode), to::setAcl);
+    ifNonNull(from.getAcl(), Utils.toImmutableListOf(objectAcl()::encode), to::setAcl);
     ifNonNull(from.getDeleteTime(), DateTime::new, to::setTimeDeleted);
     ifNonNull(from.getUpdateTime(), DateTime::new, to::setUpdated);
     ifNonNull(from.getCreateTime(), DateTime::new, to::setTimeCreated);
@@ -248,7 +247,7 @@ final class ApiaryConversions {
     ifNonNull(from.getCustomTime(), DateTime::getValue, to::setCustomTime);
     ifNonNull(from.getSize(), BigInteger::longValue, to::setSize);
     ifNonNull(from.getOwner(), lift(Owner::getEntity).andThen(this::entityDecode), to::setOwner);
-    ifNonNull(from.getAcl(), toImmutableListOf(objectAcl()::decode), to::setAcl);
+    ifNonNull(from.getAcl(), Utils.toImmutableListOf(objectAcl()::decode), to::setAcl);
     if (from.containsKey("isDirectory")) {
       to.setIsDirectory(Boolean.TRUE);
     }
@@ -289,11 +288,13 @@ final class ApiaryConversions {
 
   private Bucket bucketInfoEncode(BucketInfo from) {
     Bucket to = new Bucket();
-    ifNonNull(from.getAcl(), toImmutableListOf(bucketAcl()::encode), to::setAcl);
-    ifNonNull(from.getCors(), toImmutableListOf(cors()::encode), to::setCors);
+    ifNonNull(from.getAcl(), Utils.toImmutableListOf(bucketAcl()::encode), to::setAcl);
+    ifNonNull(from.getCors(), Utils.toImmutableListOf(cors()::encode), to::setCors);
     ifNonNull(from.getCreateTime(), DateTime::new, to::setTimeCreated);
     ifNonNull(
-        from.getDefaultAcl(), toImmutableListOf(objectAcl()::encode), to::setDefaultObjectAcl);
+        from.getDefaultAcl(),
+        Utils.toImmutableListOf(objectAcl()::encode),
+        to::setDefaultObjectAcl);
     ifNonNull(from.getLocation(), to::setLocation);
     ifNonNull(from.getLocationType(), to::setLocationType);
     ifNonNull(from.getMetageneration(), to::setMetageneration);
@@ -378,10 +379,12 @@ final class ApiaryConversions {
   @SuppressWarnings("deprecation")
   private BucketInfo bucketInfoDecode(com.google.api.services.storage.model.Bucket from) {
     BucketInfo.Builder to = new BuilderImpl(from.getName());
-    ifNonNull(from.getAcl(), toImmutableListOf(bucketAcl()::decode), to::setAcl);
-    ifNonNull(from.getCors(), toImmutableListOf(cors()::decode), to::setCors);
+    ifNonNull(from.getAcl(), Utils.toImmutableListOf(bucketAcl()::decode), to::setAcl);
+    ifNonNull(from.getCors(), Utils.toImmutableListOf(cors()::decode), to::setCors);
     ifNonNull(
-        from.getDefaultObjectAcl(), toImmutableListOf(objectAcl()::decode), to::setDefaultAcl);
+        from.getDefaultObjectAcl(),
+        Utils.toImmutableListOf(objectAcl()::decode),
+        to::setDefaultAcl);
     ifNonNull(from.getEtag(), to::setEtag);
     ifNonNull(from.getId(), to::setGeneratedId);
     ifNonNull(from.getLocation(), to::setLocation);
@@ -399,12 +402,12 @@ final class ApiaryConversions {
     ifNonNull(from.getWebsite(), Website::getNotFoundPage, to::setNotFoundPage);
     ifNonNull(
         from.getLifecycle(),
-        lift(Lifecycle::getRule).andThen(toImmutableListOf(lifecycleRule()::decode)),
+        lift(Lifecycle::getRule).andThen(Utils.toImmutableListOf(lifecycleRule()::decode)),
         to::setLifecycleRules);
     // preserve mapping to deprecated property
     ifNonNull(
         from.getLifecycle(),
-        lift(Lifecycle::getRule).andThen(toImmutableListOf(deleteRule()::decode)),
+        lift(Lifecycle::getRule).andThen(Utils.toImmutableListOf(deleteRule()::decode)),
         to::setDeleteRules);
     ifNonNull(from.getDefaultEventBasedHold(), to::setDefaultEventBasedHold);
     ifNonNull(from.getLabels(), to::setLabels);
@@ -521,7 +524,7 @@ final class ApiaryConversions {
         from.getCustomTimeBefore(), this::truncateToDateWithNoTzDrift, to::setCustomTimeBefore);
     ifNonNull(
         from.getMatchesStorageClass(),
-        toImmutableListOf(Object::toString),
+        Utils.toImmutableListOf(Object::toString),
         to::setMatchesStorageClass);
     return to;
   }
@@ -572,7 +575,7 @@ final class ApiaryConversions {
             .setDaysSinceCustomTime(condition.getDaysSinceCustomTime());
     ifNonNull(
         condition.getMatchesStorageClass(),
-        toImmutableListOf(StorageClass::valueOf),
+        Utils.toImmutableListOf(StorageClass::valueOf),
         conditionBuilder::setMatchesStorageClass);
 
     return new LifecycleRule(lifecycleAction, conditionBuilder.build());
@@ -601,8 +604,8 @@ final class ApiaryConversions {
     Bucket.Cors to = new Bucket.Cors();
     to.setMaxAgeSeconds(from.getMaxAgeSeconds());
     to.setResponseHeader(from.getResponseHeaders());
-    ifNonNull(from.getMethods(), toImmutableListOf(Object::toString), to::setMethod);
-    ifNonNull(from.getOrigins(), toImmutableListOf(Object::toString), to::setOrigin);
+    ifNonNull(from.getMethods(), Utils.toImmutableListOf(Object::toString), to::setMethod);
+    ifNonNull(from.getOrigins(), Utils.toImmutableListOf(Object::toString), to::setOrigin);
     return to;
   }
 
@@ -616,7 +619,7 @@ final class ApiaryConversions {
                 .map(HttpMethod::valueOf)
                 .collect(ImmutableList.toImmutableList()),
         to::setMethods);
-    ifNonNull(from.getOrigin(), toImmutableListOf(Origin::of), to::setOrigins);
+    ifNonNull(from.getOrigin(), Utils.toImmutableListOf(Origin::of), to::setOrigins);
     to.setResponseHeaders(from.getResponseHeader());
     return to.build();
   }
@@ -799,13 +802,4 @@ final class ApiaryConversions {
   private DateTime truncateToDateWithNoTzDrift(DateTime dt) {
     return new DateTime(true, dt.getValue(), 0);
   }
-
-  /**
-   * Several properties are translating lists of one type to another. This convenience method allows
-   * specifying a mapping function and composing as part of an {@code #isNonNull} definition.
-   */
-  private static <T1, T2> Function<List<T1>, ImmutableList<T2>> toImmutableListOf(
-      Function<T1, T2> f) {
-    return l -> l.stream().map(f).collect(ImmutableList.toImmutableList());
-  }
 }
diff --git a/google-cloud-storage/src/main/java/com/google/cloud/storage/GrpcConversions.java b/google-cloud-storage/src/main/java/com/google/cloud/storage/GrpcConversions.java
index e5278fbf..3eb23801 100644
--- a/google-cloud-storage/src/main/java/com/google/cloud/storage/GrpcConversions.java
+++ b/google-cloud-storage/src/main/java/com/google/cloud/storage/GrpcConversions.java
@@ -16,9 +16,13 @@
 
 package com.google.cloud.storage;
 
+import static com.google.cloud.storage.Utils.ifNonNull;
 import static com.google.cloud.storage.Utils.todo;
 
 import com.google.cloud.storage.Conversions.Codec;
+import com.google.protobuf.util.Timestamps;
+import com.google.storage.v2.Bucket;
+import com.google.storage.v2.Bucket.Billing;
 import com.google.storage.v2.Object;
 
 final class GrpcConversions {
@@ -35,7 +39,8 @@ final class GrpcConversions {
   private final Codec<?, ?> iamConfigurationCodec = Codec.of(Utils::todo, Utils::todo);
   private final Codec<?, ?> lifecycleRuleCodec = Codec.of(Utils::todo, Utils::todo);
   private final Codec<?, ?> deleteRuleCodec = Codec.of(Utils::todo, Utils::todo);
-  private final Codec<?, ?> bucketInfoCodec = Codec.of(Utils::todo, Utils::todo);
+  private final Codec<BucketInfo, Bucket> bucketInfoCodec =
+      Codec.of(Utils::todo, this::bucketInfoDecode);
   private final Codec<?, ?> customerEncryptionCodec = Codec.of(Utils::todo, Utils::todo);
   private final Codec<BlobId, Object> blobIdCodec = Codec.of(Utils::todo, Utils::todo);
   private final Codec<?, ?> blobInfoCodec = Codec.of(Utils::todo, Utils::todo);
@@ -87,8 +92,8 @@ final class GrpcConversions {
     return todo();
   }
 
-  Codec<?, ?> bucketInfo() {
-    return todo();
+  Codec<BucketInfo, Bucket> bucketInfo() {
+    return bucketInfoCodec;
   }
 
   Codec<?, ?> customerEncryption() {
@@ -106,4 +111,40 @@ final class GrpcConversions {
   Codec<?, ?> notificationInfo() {
     return todo();
   }
+
+  private BucketInfo bucketInfoDecode(Bucket from) {
+    BucketInfo.Builder to = BucketInfo.newBuilder(from.getName());
+    Bucket.RetentionPolicy retentionPolicy = from.getRetentionPolicy();
+    ifNonNull(retentionPolicy, Bucket.RetentionPolicy::getIsLocked, to::setRetentionPolicyIsLocked);
+    ifNonNull(retentionPolicy, Bucket.RetentionPolicy::getRetentionPeriod, to::setRetentionPeriod);
+    if (from.hasRetentionPolicy() && retentionPolicy.hasEffectiveTime()) {
+      to.setRetentionEffectiveTime(retentionPolicy.getEffectiveTime().getSeconds());
+    }
+    ifNonNull(from.getBucketId(), to::setGeneratedId);
+    ifNonNull(from.getLocation(), to::setLocation);
+    ifNonNull(from.getLocationType(), to::setLocationType);
+    ifNonNull(from.getMetageneration(), to::setMetageneration);
+    ifNonNull(from.getBilling(), Billing::getRequesterPays, to::setRequesterPays);
+    ifNonNull(from.getCreateTime(), Timestamps::toMillis, to::setCreateTime);
+    ifNonNull(from.getUpdateTime(), Timestamps::toMillis, to::setUpdateTime);
+    ifNonNull(from.getEncryption(), Bucket.Encryption::getDefaultKmsKey, to::setDefaultKmsKeyName);
+    ifNonNull(from.getWebsite(), Bucket.Website::getMainPageSuffix, to::setIndexPage);
+    ifNonNull(from.getWebsite(), Bucket.Website::getNotFoundPage, to::setNotFoundPage);
+    ifNonNull(from.getRpo(), Rpo::valueOf, to::setRpo);
+    ifNonNull(from.getStorageClass(), StorageClass::valueOf, to::setStorageClass);
+    ifNonNull(from.getVersioning(), Bucket.Versioning::getEnabled, to::setVersioningEnabled);
+    ifNonNull(from.getDefaultEventBasedHold(), to::setDefaultEventBasedHold);
+    ifNonNull(from.getLabels(), to::setLabels);
+    ifNonNull(from.getBilling(), Bucket.Billing::getRequesterPays, to::setRequesterPays);
+    // TODO(frankyn): Add logging decoder
+    // TODO(frankyn): Add entity decoder
+    // TODO(frankyn): Add lifeycle decoder
+    // TODO(frankyn): Add deleteRules decoder
+    // TODO(frankyn): Add SelfLink when the field is available
+    // TODO(frankyn): Add decoder for iamConfig
+    // TODO(frankyn): Add Etag when support is avialable
+    // TODO(frankyn): Add Cors decoder support
+    // TODO(frnakyn): Add DefaultObjectAcl decoder support
+    return to.build();
+  }
 }
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 015985a4..e5a3ec5b 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
@@ -16,9 +16,11 @@
 
 package com.google.cloud.storage;
 
+import static com.google.cloud.storage.Utils.ifNonNull;
 import static com.google.cloud.storage.Utils.todo;
 
 import com.google.api.gax.paging.Page;
+import com.google.api.gax.rpc.UnaryCallable;
 import com.google.cloud.BaseService;
 import com.google.cloud.Policy;
 import com.google.cloud.ReadChannel;
@@ -28,6 +30,8 @@ import com.google.cloud.storage.HmacKey.HmacKeyMetadata;
 import com.google.cloud.storage.HmacKey.HmacKeyState;
 import com.google.cloud.storage.PostPolicyV4.PostConditionsV4;
 import com.google.cloud.storage.PostPolicyV4.PostFieldsV4;
+import com.google.cloud.storage.spi.v1.StorageRpc;
+import com.google.storage.v2.GetBucketRequest;
 import com.google.storage.v2.stub.GrpcStorageStub;
 import java.io.IOException;
 import java.io.InputStream;
@@ -35,6 +39,7 @@ import java.io.OutputStream;
 import java.net.URL;
 import java.nio.file.Path;
 import java.util.List;
+import java.util.Map;
 import java.util.concurrent.TimeUnit;
 
 final class GrpcStorageImpl extends BaseService<StorageOptions> implements Storage {
@@ -103,7 +108,20 @@ final class GrpcStorageImpl extends BaseService<StorageOptions> implements Stora
 
   @Override
   public Bucket get(String bucket, BucketGetOption... options) {
-    return todo();
+    UnaryCallable<GetBucketRequest, com.google.storage.v2.Bucket> bucketCallable =
+        grpcStorageStub.getBucketCallable();
+    final Map<StorageRpc.Option, ?> optionsMap = StorageImpl.optionMap(options);
+    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);
+    // TODO(frankyn): Do we care about projection because Apiary uses FULL for projection? Missing
+    // projection=full
+    com.google.storage.v2.Bucket protoBucket = bucketCallable.call(bucketRequestBuilder.build());
+    return Conversions.grpc().bucketInfo().decode(protoBucket).asBucket(this);
   }
 
   @Override
diff --git a/google-cloud-storage/src/main/java/com/google/cloud/storage/StorageImpl.java b/google-cloud-storage/src/main/java/com/google/cloud/storage/StorageImpl.java
index 33a57cb5..40673d2d 100644
--- a/google-cloud-storage/src/main/java/com/google/cloud/storage/StorageImpl.java
+++ b/google-cloud-storage/src/main/java/com/google/cloud/storage/StorageImpl.java
@@ -1628,7 +1628,7 @@ final class StorageImpl extends BaseService<StorageOptions> implements Storage {
     return ImmutableMap.copyOf(temp);
   }
 
-  private static Map<StorageRpc.Option, ?> optionMap(Option... options) {
+  static Map<StorageRpc.Option, ?> optionMap(Option... options) {
     return optionMap(null, null, Arrays.asList(options));
   }
 
diff --git a/google-cloud-storage/src/main/java/com/google/cloud/storage/Utils.java b/google-cloud-storage/src/main/java/com/google/cloud/storage/Utils.java
index 64224134..bc6bfb33 100644
--- a/google-cloud-storage/src/main/java/com/google/cloud/storage/Utils.java
+++ b/google-cloud-storage/src/main/java/com/google/cloud/storage/Utils.java
@@ -17,6 +17,8 @@
 package com.google.cloud.storage;
 
 import com.google.api.core.InternalApi;
+import com.google.common.collect.ImmutableList;
+import java.util.List;
 import java.util.function.Consumer;
 import java.util.function.Function;
 import javax.annotation.Nullable;
@@ -82,6 +84,15 @@ final class Utils {
     return f;
   }
 
+  /**
+   * Several properties are translating lists of one type to another. This convenience method allows
+   * specifying a mapping function and composing as part of an {@code #isNonNull} definition.
+   */
+  @InternalApi
+  static <T1, T2> Function<List<T1>, ImmutableList<T2>> toImmutableListOf(Function<T1, T2> f) {
+    return l -> l.stream().map(f).collect(ImmutableList.toImmutableList());
+  }
+
   static final <T> T todo() {
     throw new IllegalStateException("Not yet implemented");
   }


Test patch:
diff --git a/google-cloud-storage/src/test/java/com/google/cloud/storage/BucketPropertyTest.java b/google-cloud-storage/src/test/java/com/google/cloud/storage/BucketPropertyTest.java
new file mode 100644
index 00000000..ca1382ec
--- /dev/null
+++ b/google-cloud-storage/src/test/java/com/google/cloud/storage/BucketPropertyTest.java
@@ -0,0 +1,48 @@
+/*
+ * 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 com.google.protobuf.util.Timestamps;
+import com.google.storage.v2.Bucket;
+import net.jqwik.api.ForAll;
+import net.jqwik.api.Property;
+
+final class BucketPropertyTest {
+
+  @Property
+  void allBucketsDecode(@ForAll Bucket b) {
+    BucketInfo decoded = Conversions.grpc().bucketInfo().decode(b);
+    assertThat(decoded.getGeneratedId()).isEqualTo(b.getBucketId());
+    assertThat(decoded.getName()).isEqualTo(b.getName());
+    assertThat(decoded.getMetageneration()).isEqualTo(b.getMetageneration());
+    assertThat(decoded.getStorageClass().toString()).isEqualTo(b.getStorageClass());
+    assertThat(decoded.getLocation()).isEqualTo(b.getLocation());
+    assertThat(decoded.getLocationType()).isEqualTo(b.getLocationType());
+    assertThat(decoded.getCreateTime()).isEqualTo(Timestamps.toMillis(b.getCreateTime()));
+    assertThat(decoded.getUpdateTime()).isEqualTo(Timestamps.toMillis(b.getUpdateTime()));
+    assertThat(decoded.versioningEnabled()).isEqualTo(b.getVersioning().getEnabled());
+    assertThat(decoded.getRpo().toString()).isEqualTo(b.getRpo());
+    assertThat(decoded.requesterPays()).isEqualTo(b.getBilling().getRequesterPays());
+    assertThat(decoded.getDefaultKmsKeyName()).isEqualTo(b.getEncryption().getDefaultKmsKey());
+    if (b.getWebsite() != null) {
+      assertThat(decoded.getIndexPage()).isEqualTo(b.getWebsite().getMainPageSuffix());
+      assertThat(decoded.getNotFoundPage()).isEqualTo(b.getWebsite().getNotFoundPage());
+    }
+  }
+}
diff --git a/google-cloud-storage/src/test/java/com/google/cloud/storage/JqwikTest.java b/google-cloud-storage/src/test/java/com/google/cloud/storage/JqwikTest.java
index 8557c5f2..664e23a3 100644
--- a/google-cloud-storage/src/test/java/com/google/cloud/storage/JqwikTest.java
+++ b/google-cloud-storage/src/test/java/com/google/cloud/storage/JqwikTest.java
@@ -37,6 +37,8 @@ public class JqwikTest {
     report(of(List.class).asNullable());
     report(of(Map.class));
     report(of(Map.class).asNullable());
+    report(of(Boolean.class));
+    report(of(Boolean.class).asNullable());
   }
 
   public static void report(TypeUsage t) {
diff --git a/google-cloud-storage/src/test/java/com/google/cloud/storage/jqwik/BucketArbitraryProvider.java b/google-cloud-storage/src/test/java/com/google/cloud/storage/jqwik/BucketArbitraryProvider.java
new file mode 100644
index 00000000..4c478172
--- /dev/null
+++ b/google-cloud-storage/src/test/java/com/google/cloud/storage/jqwik/BucketArbitraryProvider.java
@@ -0,0 +1,83 @@
+/*
+ * 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.jqwik;
+
+import com.google.storage.v2.Bucket;
+import java.util.Collections;
+import java.util.Set;
+import net.jqwik.api.Arbitraries;
+import net.jqwik.api.Arbitrary;
+import net.jqwik.api.Combinators;
+import net.jqwik.api.Tuple;
+import net.jqwik.api.providers.ArbitraryProvider;
+import net.jqwik.api.providers.TypeUsage;
+
+public final class BucketArbitraryProvider implements ArbitraryProvider {
+
+  @Override
+  public boolean canProvideFor(TypeUsage targetType) {
+    return targetType.isOfType(Bucket.class);
+  }
+
+  @Override
+  public Set<Arbitrary<?>> provideFor(TypeUsage targetType, SubtypeProvider subtypeProvider) {
+    Arbitrary<String> bucketID = Arbitraries.strings().all().ofMinLength(0).ofLength(1024);
+    Arbitrary<String> location = Arbitraries.strings().all().ofMinLength(1).ofMaxLength(1024);
+    Arbitrary<String> locationType = Arbitraries.strings().all().ofMinLength(1).ofMaxLength(1024);
+    Arbitrary<Bucket> as =
+        Combinators.combine(
+                Combinators.combine(
+                        bucketID,
+                        StorageArbitraries.bucketName(),
+                        StorageArbitraries.buckets().storageClass(),
+                        location,
+                        locationType,
+                        StorageArbitraries.metageneration(),
+                        StorageArbitraries.buckets().versioning(),
+                        StorageArbitraries.timestamp())
+                    .as(Tuple::of),
+                Combinators.combine(
+                        StorageArbitraries.timestamp(),
+                        StorageArbitraries.buckets().website(),
+                        StorageArbitraries.bool(),
+                        StorageArbitraries.buckets().rpo(),
+                        StorageArbitraries.buckets().billing(),
+                        StorageArbitraries.buckets().encryption(),
+                        StorageArbitraries.buckets().retentionPolicy())
+                    .as(Tuple::of))
+            .as(
+                (t1, t2) ->
+                    Bucket.newBuilder()
+                        .setBucketId(t1.get1())
+                        .setName(t1.get2().get())
+                        .setStorageClass(t1.get3())
+                        .setLocation(t1.get4())
+                        .setLocationType(t1.get5())
+                        .setMetageneration(t1.get6())
+                        .setVersioning(t1.get7())
+                        .setCreateTime(t1.get8())
+                        .setUpdateTime(t2.get1())
+                        .setWebsite(t2.get2())
+                        .setDefaultEventBasedHold(t2.get3())
+                        .setRpo(t2.get4())
+                        .setBilling(t2.get5())
+                        .setEncryption(t2.get6())
+                        .setRetentionPolicy(t2.get7())
+                        .build());
+    return Collections.singleton(as);
+  }
+}
diff --git a/google-cloud-storage/src/test/java/com/google/cloud/storage/jqwik/StorageArbitraries.java b/google-cloud-storage/src/test/java/com/google/cloud/storage/jqwik/StorageArbitraries.java
new file mode 100644
index 00000000..dcf02cd9
--- /dev/null
+++ b/google-cloud-storage/src/test/java/com/google/cloud/storage/jqwik/StorageArbitraries.java
@@ -0,0 +1,140 @@
+/*
+ * 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.jqwik;
+
+import com.google.protobuf.Timestamp;
+import com.google.protobuf.util.Timestamps;
+import com.google.storage.v2.Bucket;
+import com.google.storage.v2.Bucket.Billing;
+import com.google.storage.v2.Bucket.Encryption;
+import com.google.storage.v2.Bucket.RetentionPolicy;
+import com.google.storage.v2.Bucket.Versioning;
+import com.google.storage.v2.Bucket.Website;
+import net.jqwik.api.Arbitraries;
+import net.jqwik.api.Arbitrary;
+import net.jqwik.api.Combinators;
+import net.jqwik.api.providers.TypeUsage;
+
+public final class StorageArbitraries {
+
+  private StorageArbitraries() {}
+
+  public static Arbitrary<Timestamp> timestamp() {
+    return Arbitraries.longs().between(0, 100000000L).map(s -> Timestamps.fromMillis(s));
+  }
+
+  public static Arbitrary<Boolean> bool() {
+    return Arbitraries.defaultFor(TypeUsage.of(Boolean.class));
+  }
+
+  public static Arbitrary<Long> metageneration() {
+    return Arbitraries.longs().greaterOrEqual(0);
+  }
+
+  /**
+   * Generated bucket name based on the rules outlined in <a target="_blank" rel="noopener
+   * noreferrer"
+   * href="https://cloud.google.com/storage/docs/naming-buckets#requirements">https://cloud.google.com/storage/docs/naming-buckets#requirements</a>
+   */
+  public static Arbitrary<BucketName> bucketName() {
+    return Combinators.combine(
+            Arbitraries.oneOf(Arbitraries.chars().alpha(), Arbitraries.chars().numeric()),
+            Arbitraries.oneOf(
+                    Arbitraries.chars().alpha(),
+                    Arbitraries.chars().numeric(),
+                    Arbitraries.chars().with('-', '_'))
+                .list()
+                .ofMinSize(1)
+                .ofMaxSize(61),
+            Arbitraries.oneOf(Arbitraries.chars().alpha(), Arbitraries.chars().numeric()))
+        .as(
+            (first, mid, last) -> {
+              final StringBuilder sb = new StringBuilder();
+              sb.append(first);
+              mid.forEach(sb::append);
+              sb.append(last);
+
+              return new BucketName(sb.toString());
+            });
+  }
+
+  public static Buckets buckets() {
+    return Buckets.INSTANCE;
+  }
+
+  public static final class Buckets {
+    private static final Buckets INSTANCE = new Buckets();
+
+    private Buckets() {}
+
+    public Arbitrary<Website> website() {
+      Arbitrary<String> indexPage = Arbitraries.strings().all().ofMinLength(1).ofMaxLength(1024);
+      Arbitrary<String> notFoundPage = Arbitraries.strings().all().ofMinLength(1).ofMaxLength(1024);
+      return Combinators.combine(indexPage, notFoundPage)
+          .as((i, n) -> Website.newBuilder().setMainPageSuffix(i).setNotFoundPage(n).build());
+    }
+
+    public Arbitrary<Bucket.Billing> billing() {
+      return bool().map(b -> Billing.newBuilder().setRequesterPays(b).build());
+    }
+
+    public Arbitrary<Bucket.Encryption> encryption() {
+      return Arbitraries.strings()
+          .all()
+          .ofMinLength(1)
+          .ofMaxLength(1024)
+          .map(s -> Encryption.newBuilder().setDefaultKmsKey(s).build());
+    }
+
+    public Arbitrary<Bucket.RetentionPolicy> retentionPolicy() {
+      return Combinators.combine(bool(), Arbitraries.longs().greaterOrEqual(0), timestamp())
+          .as(
+              (locked, period, effectiveTime) ->
+                  RetentionPolicy.newBuilder()
+                      .setIsLocked(locked)
+                      .setRetentionPeriod(period)
+                      .setEffectiveTime(effectiveTime)
+                      .build());
+    }
+
+    public Arbitrary<Bucket.Versioning> versioning() {
+      return bool().map(b -> Versioning.newBuilder().setEnabled(b).build());
+    }
+
+    public Arbitrary<String> storageClass() {
+      // TODO: return each of the real values and edge cases (including invalid values)
+      return Arbitraries.strings().all().ofMinLength(1).ofLength(1024);
+    }
+
+    public Arbitrary<String> rpo() {
+      // TODO: return each of the real values and edge cases (including invalid values)
+      return Arbitraries.strings().all().ofMinLength(1).ofLength(1024);
+    }
+  }
+
+  public static final class BucketName {
+    private final String value;
+
+    private BucketName(String value) {
+      this.value = value;
+    }
+
+    public String get() {
+      return value;
+    }
+  }
+}
diff --git a/google-cloud-storage/src/test/resources/META-INF/services/net.jqwik.api.providers.ArbitraryProvider b/google-cloud-storage/src/test/resources/META-INF/services/net.jqwik.api.providers.ArbitraryProvider
index 29bbea8e..755775cb 100644
--- a/google-cloud-storage/src/test/resources/META-INF/services/net.jqwik.api.providers.ArbitraryProvider
+++ b/google-cloud-storage/src/test/resources/META-INF/services/net.jqwik.api.providers.ArbitraryProvider
@@ -15,3 +15,4 @@
 #
 
 com.google.cloud.storage.jqwik.ObjectArbitraryProvider
+com.google.cloud.storage.jqwik.BucketArbitraryProvider
\ No newline at end of file