This is an automated email from the ASF dual-hosted git repository.

alamb pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/arrow-rs-object-store.git


The following commit(s) were added to refs/heads/main by this push:
     new f0a772c  feat: refactor GetOptions with builder, add binary examples 
(#517)
f0a772c is described below

commit f0a772cd49d2ebb1f19f487ccd93d705f48dc891
Author: peasee <[email protected]>
AuthorDate: Sun Oct 26 20:40:22 2025 +1000

    feat: refactor GetOptions with builder, add binary examples (#517)
    
    * feat: Add get_range_opts, refactor GetOptions with builder
    
    * test: Fix test
    
    * refactor: Remove _opts functions, cargo fmt
    
    * chore: Clippy
    
    * docs: Add examples
    
    * fix: CI
    
    * fix: Silly wasm things
    
    * docs: Add examples inline to docs
    
    * fix: Update builder inputs to options
    
    * test: fix tests
    
    * fix: Bang my head on my keyboard
    
    * fix: Revert change trying to remove docs tests from wasm test
---
 Cargo.toml              |   2 +-
 src/buffered.rs         |  32 +----
 src/integration.rs      |  96 ++++----------
 src/lib.rs              | 334 ++++++++++++++++++++++++++++++++++++++++++++++--
 tests/get_range_file.rs |  33 ++++-
 tests/http.rs           |  10 +-
 6 files changed, 381 insertions(+), 126 deletions(-)

diff --git a/Cargo.toml b/Cargo.toml
index 015e50f..137d2c5 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -105,4 +105,4 @@ features = ["js"]
 [[test]]
 name = "get_range_file"
 path = "tests/get_range_file.rs"
-required-features = ["fs"]
+required-features = ["fs"]
\ No newline at end of file
diff --git a/src/buffered.rs b/src/buffered.rs
index 00bea05..9ec3285 100644
--- a/src/buffered.rs
+++ b/src/buffered.rs
@@ -590,13 +590,7 @@ mod tests {
         writer.write_all(&[0; 5]).await.unwrap();
         writer.shutdown().await.unwrap();
         let response = store
-            .get_opts(
-                &path,
-                GetOptions {
-                    head: true,
-                    ..Default::default()
-                },
-            )
+            .get_opts(&path, GetOptions::new().with_head(true))
             .await
             .unwrap();
         assert_eq!(response.meta.size, 25);
@@ -610,13 +604,7 @@ mod tests {
         writer.write_all(&[0; 20]).await.unwrap();
         writer.shutdown().await.unwrap();
         let response = store
-            .get_opts(
-                &path,
-                GetOptions {
-                    head: true,
-                    ..Default::default()
-                },
-            )
+            .get_opts(&path, GetOptions::new().with_head(true))
             .await
             .unwrap();
         assert_eq!(response.meta.size, 40);
@@ -640,13 +628,7 @@ mod tests {
             .unwrap();
         writer.shutdown().await.unwrap();
         let response = store
-            .get_opts(
-                &path,
-                GetOptions {
-                    head: true,
-                    ..Default::default()
-                },
-            )
+            .get_opts(&path, GetOptions::new().with_head(true))
             .await
             .unwrap();
         assert_eq!(response.meta.size, 25);
@@ -664,13 +646,7 @@ mod tests {
             .unwrap();
         writer.shutdown().await.unwrap();
         let response = store
-            .get_opts(
-                &path,
-                GetOptions {
-                    head: true,
-                    ..Default::default()
-                },
-            )
+            .get_opts(&path, GetOptions::new().with_head(true))
             .await
             .unwrap();
         assert_eq!(response.meta.size, 40);
diff --git a/src/integration.rs b/src/integration.rs
index 316986c..1f769b1 100644
--- a/src/integration.rs
+++ b/src/integration.rs
@@ -114,10 +114,7 @@ pub async fn put_get_delete_list(storage: &DynObjectStore) 
{
     let bytes = range_result.unwrap();
     assert_eq!(bytes, data.slice(range.start as usize..range.end as usize));
 
-    let opts = GetOptions {
-        range: Some(GetRange::Bounded(2..5)),
-        ..Default::default()
-    };
+    let opts = GetOptions::new().with_range(Some(GetRange::Bounded(2..5)));
     let result = storage.get_opts(&location, opts).await.unwrap();
     // Data is `"arbitrary data"`, length 14 bytes
     assert_eq!(result.meta.size, 14); // Should return full object size (#5272)
@@ -131,20 +128,14 @@ pub async fn put_get_delete_list(storage: 
&DynObjectStore) {
     // Should be a non-fatal error
     out_of_range_result.unwrap_err();
 
-    let opts = GetOptions {
-        range: Some(GetRange::Bounded(2..100)),
-        ..Default::default()
-    };
+    let opts = GetOptions::new().with_range(Some(GetRange::Bounded(2..100)));
     let result = storage.get_opts(&location, opts).await.unwrap();
     assert_eq!(result.range, 2..14);
     assert_eq!(result.meta.size, 14);
     let bytes = result.bytes().await.unwrap();
     assert_eq!(bytes, b"bitrary data".as_ref());
 
-    let opts = GetOptions {
-        range: Some(GetRange::Suffix(2)),
-        ..Default::default()
-    };
+    let opts = GetOptions::new().with_range(Some(GetRange::Suffix(2)));
     match storage.get_opts(&location, opts).await {
         Ok(result) => {
             assert_eq!(result.range, 12..14);
@@ -156,10 +147,7 @@ pub async fn put_get_delete_list(storage: &DynObjectStore) 
{
         Err(e) => panic!("{e}"),
     }
 
-    let opts = GetOptions {
-        range: Some(GetRange::Suffix(100)),
-        ..Default::default()
-    };
+    let opts = GetOptions::new().with_range(Some(GetRange::Suffix(100)));
     match storage.get_opts(&location, opts).await {
         Ok(result) => {
             assert_eq!(result.range, 0..14);
@@ -171,20 +159,14 @@ pub async fn put_get_delete_list(storage: 
&DynObjectStore) {
         Err(e) => panic!("{e}"),
     }
 
-    let opts = GetOptions {
-        range: Some(GetRange::Offset(3)),
-        ..Default::default()
-    };
+    let opts = GetOptions::new().with_range(Some(GetRange::Offset(3)));
     let result = storage.get_opts(&location, opts).await.unwrap();
     assert_eq!(result.range, 3..14);
     assert_eq!(result.meta.size, 14);
     let bytes = result.bytes().await.unwrap();
     assert_eq!(bytes, b"itrary data".as_ref());
 
-    let opts = GetOptions {
-        range: Some(GetRange::Offset(100)),
-        ..Default::default()
-    };
+    let opts = GetOptions::new().with_range(Some(GetRange::Offset(100)));
     storage.get_opts(&location, opts).await.unwrap_err();
 
     let ranges = vec![0..1, 2..3, 0..5];
@@ -520,76 +502,55 @@ pub async fn get_opts(storage: &dyn ObjectStore) {
     storage.put(&path, "foo".into()).await.unwrap();
     let meta = storage.head(&path).await.unwrap();
 
-    let options = GetOptions {
-        if_unmodified_since: Some(meta.last_modified),
-        ..GetOptions::default()
-    };
+    let options = 
GetOptions::new().with_if_unmodified_since(Some(meta.last_modified));
     match storage.get_opts(&path, options).await {
         Ok(_) | Err(Error::NotSupported { .. }) => {}
         Err(e) => panic!("{e}"),
     }
 
-    let options = GetOptions {
-        if_unmodified_since: Some(meta.last_modified + 
chrono::Duration::try_hours(10).unwrap()),
-        ..GetOptions::default()
-    };
+    let options = GetOptions::new().with_if_unmodified_since(Some(
+        meta.last_modified + chrono::Duration::try_hours(10).unwrap(),
+    ));
     match storage.get_opts(&path, options).await {
         Ok(_) | Err(Error::NotSupported { .. }) => {}
         Err(e) => panic!("{e}"),
     }
 
-    let options = GetOptions {
-        if_unmodified_since: Some(meta.last_modified - 
chrono::Duration::try_hours(10).unwrap()),
-        ..GetOptions::default()
-    };
+    let options = GetOptions::new().with_if_unmodified_since(Some(
+        meta.last_modified - chrono::Duration::try_hours(10).unwrap(),
+    ));
     match storage.get_opts(&path, options).await {
         Err(Error::Precondition { .. } | Error::NotSupported { .. }) => {}
         d => panic!("{d:?}"),
     }
 
-    let options = GetOptions {
-        if_modified_since: Some(meta.last_modified),
-        ..GetOptions::default()
-    };
+    let options = 
GetOptions::new().with_if_modified_since(Some(meta.last_modified));
     match storage.get_opts(&path, options).await {
         Err(Error::NotModified { .. } | Error::NotSupported { .. }) => {}
         d => panic!("{d:?}"),
     }
 
-    let options = GetOptions {
-        if_modified_since: Some(meta.last_modified - 
chrono::Duration::try_hours(10).unwrap()),
-        ..GetOptions::default()
-    };
+    let options = GetOptions::new().with_if_modified_since(Some(
+        meta.last_modified - chrono::Duration::try_hours(10).unwrap(),
+    ));
     match storage.get_opts(&path, options).await {
         Ok(_) | Err(Error::NotSupported { .. }) => {}
         Err(e) => panic!("{e}"),
     }
 
     let tag = meta.e_tag.unwrap();
-    let options = GetOptions {
-        if_match: Some(tag.clone()),
-        ..GetOptions::default()
-    };
+    let options = GetOptions::new().with_if_match(Some(tag.clone()));
     storage.get_opts(&path, options).await.unwrap();
 
-    let options = GetOptions {
-        if_match: Some("invalid".to_string()),
-        ..GetOptions::default()
-    };
+    let options = GetOptions::new().with_if_match(Some("invalid".to_string()));
     let err = storage.get_opts(&path, options).await.unwrap_err();
     assert!(matches!(err, Error::Precondition { .. }), "{err}");
 
-    let options = GetOptions {
-        if_none_match: Some(tag.clone()),
-        ..GetOptions::default()
-    };
+    let options = GetOptions::new().with_if_none_match(Some(tag.clone()));
     let err = storage.get_opts(&path, options).await.unwrap_err();
     assert!(matches!(err, Error::NotModified { .. }), "{err}");
 
-    let options = GetOptions {
-        if_none_match: Some("invalid".to_string()),
-        ..GetOptions::default()
-    };
+    let options = 
GetOptions::new().with_if_none_match(Some("invalid".to_string()));
     storage.get_opts(&path, options).await.unwrap();
 
     let result = storage.put(&path, "test".into()).await.unwrap();
@@ -599,26 +560,17 @@ pub async fn get_opts(storage: &dyn ObjectStore) {
     let meta = storage.head(&path).await.unwrap();
     assert_eq!(meta.e_tag.unwrap(), new_tag);
 
-    let options = GetOptions {
-        if_match: Some(new_tag),
-        ..GetOptions::default()
-    };
+    let options = GetOptions::new().with_if_match(Some(new_tag.clone()));
     storage.get_opts(&path, options).await.unwrap();
 
-    let options = GetOptions {
-        if_match: Some(tag),
-        ..GetOptions::default()
-    };
+    let options = GetOptions::new().with_if_match(Some(tag));
     let err = storage.get_opts(&path, options).await.unwrap_err();
     assert!(matches!(err, Error::Precondition { .. }), "{err}");
 
     if let Some(version) = meta.version {
         storage.put(&path, "bar".into()).await.unwrap();
 
-        let options = GetOptions {
-            version: Some(version),
-            ..GetOptions::default()
-        };
+        let options = GetOptions::new().with_version(Some(version));
 
         // Can retrieve previous version
         let get_opts = storage.get_opts(&path, options).await.unwrap();
diff --git a/src/lib.rs b/src/lib.rs
index bb9f8b1..e37fb69 100644
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -324,6 +324,32 @@
 //! # }
 //! ```
 //!
+//! To retrieve ranges from a versioned object, use [`ObjectStore::get_opts`] 
by specifying the range in the [`GetOptions`].
+//!
+//! ```ignore-wasm32
+//! # use object_store::local::LocalFileSystem;
+//! # use object_store::ObjectStore;
+//! # use object_store::GetOptions;
+//! # use std::sync::Arc;
+//! # use bytes::Bytes;
+//! # use tokio::io::AsyncWriteExt;
+//! # use object_store::path::Path;
+//! # fn get_object_store() -> Arc<dyn ObjectStore> {
+//! #   Arc::new(LocalFileSystem::new())
+//! # }
+//! # async fn get_range_with_options() {
+//! #
+//! let object_store: Arc<dyn ObjectStore> = get_object_store();
+//! let path = Path::from("data/large_file");
+//! let ranges = vec![90..100, 400..600, 0..10];
+//! for range in ranges {
+//!     let opts = GetOptions::default().with_range(Some(range));
+//!     let data = object_store.get_opts(&path, opts).await.unwrap();
+//!     // Do something with the data
+//! }
+//! # }
+//! ``````
+//!
 //! # Vectored Write
 //!
 //! When writing data it is often the case that the size of the output is not 
known ahead of time.
@@ -403,10 +429,7 @@
 //!             Some(e) => match e.refreshed_at.elapsed() < 
Duration::from_secs(10) {
 //!                 true => e.data.clone(), // Return cached data
 //!                 false => { // Check if remote version has changed
-//!                     let opts = GetOptions {
-//!                         if_none_match: Some(e.e_tag.clone()),
-//!                         ..GetOptions::default()
-//!                     };
+//!                     let opts = 
GetOptions::new().with_if_none_match(Some(e.e_tag.clone()));
 //!                     match self.store.get_opts(&path, opts).await {
 //!                         Ok(d) => e.data = d.bytes().await?,
 //!                         Err(Error::NotModified { .. }) => {} // Data has 
not changed
@@ -634,22 +657,196 @@ pub trait ObjectStore: std::fmt::Display + Send + Sync + 
Debug + 'static {
     ) -> Result<Box<dyn MultipartUpload>>;
 
     /// Return the bytes that are stored at the specified location.
+    ///
+    /// ## Example
+    ///
+    /// This example uses a basic local filesystem object store to get an 
object.
+    ///
+    /// ```ignore-wasm32
+    /// # use object_store::local::LocalFileSystem;
+    /// # use tempfile::tempdir;
+    /// # use object_store::{path::Path, ObjectStore};
+    /// async fn get_example() {
+    ///     let tmp = tempdir().unwrap();
+    ///     let store = LocalFileSystem::new_with_prefix(tmp.path()).unwrap();
+    ///     let location = Path::from("example.txt");
+    ///     let content = b"Hello, Object Store!";
+    ///
+    ///     // Put the object into the store
+    ///     store
+    ///         .put(&location, content.as_ref().into())
+    ///         .await
+    ///         .expect("Failed to put object");
+    ///
+    ///     // Get the object from the store
+    ///     let get_result = store.get(&location).await.expect("Failed to get 
object");
+    ///     let bytes = get_result.bytes().await.expect("Failed to read 
bytes");
+    ///     println!("Retrieved content: {}", String::from_utf8_lossy(&bytes));
+    /// }
+    /// ```
     async fn get(&self, location: &Path) -> Result<GetResult> {
         self.get_opts(location, GetOptions::default()).await
     }
 
     /// Perform a get request with options
+    ///
+    /// ## Example
+    ///
+    /// This example uses a basic local filesystem object store to get an 
object with a specific etag.
+    /// On the local filesystem, supplying an invalid etag will error.
+    /// Versioned object stores will return the specified object version, if 
it exists.
+    ///
+    /// ```ignore-wasm32
+    /// # use object_store::local::LocalFileSystem;
+    /// # use tempfile::tempdir;
+    /// # use object_store::{path::Path, ObjectStore, GetOptions};
+    /// async fn get_opts_example() {
+    ///     let tmp = tempdir().unwrap();
+    ///     let store = LocalFileSystem::new_with_prefix(tmp.path()).unwrap();
+    ///     let location = Path::from("example.txt");
+    ///     let content = b"Hello, Object Store!";
+    ///
+    ///     // Put the object into the store
+    ///     store
+    ///         .put(&location, content.as_ref().into())
+    ///         .await
+    ///         .expect("Failed to put object");
+    ///
+    ///     // Get the object from the store to figure out the right etag
+    ///     let result: object_store::GetResult = 
store.get(&location).await.expect("Failed to get object");
+    ///
+    ///     let etag = result.meta.e_tag.expect("ETag should be present");
+    ///
+    ///     // Get the object from the store with range and etag
+    ///     let bytes = store
+    ///         .get_opts(
+    ///             &location,
+    ///             GetOptions::new()
+    ///                 .with_if_match(Some(etag.clone())),
+    ///         )
+    ///         .await
+    ///         .expect("Failed to get object with range and etag")
+    ///         .bytes()
+    ///         .await
+    ///         .expect("Failed to read bytes");
+    ///
+    ///     println!(
+    ///         "Retrieved with ETag {}: {}",
+    ///         etag,
+    ///         String::from_utf8_lossy(&bytes)
+    ///     );
+    ///
+    ///     // Show that if the etag does not match, we get an error
+    ///     let wrong_etag = "wrong-etag".to_string();
+    ///     match store
+    ///         .get_opts(
+    ///             &location,
+    ///             GetOptions::new().with_if_match(Some(wrong_etag))
+    ///         )
+    ///         .await
+    ///     {
+    ///         Ok(_) => println!("Unexpectedly succeeded with wrong ETag"),
+    ///         Err(e) => println!("On a non-versioned object store, getting 
an invalid ETag ('wrong-etag') results in an error as expected: {}", e),
+    ///     }
+    /// }
+    /// ```
+    ///
+    /// To retrieve a range of bytes from a versioned object, specify the 
range in the [`GetOptions`] supplied to this method.
+    ///
+    /// ```ignore-wasm32
+    /// # use object_store::local::LocalFileSystem;
+    /// # use tempfile::tempdir;
+    /// # use object_store::{path::Path, ObjectStore, GetOptions};
+    /// async fn get_opts_range_example() {
+    ///     let tmp = tempdir().unwrap();
+    ///     let store = LocalFileSystem::new_with_prefix(tmp.path()).unwrap();
+    ///     let location = Path::from("example.txt");
+    ///     let content = b"Hello, Object Store!";
+    ///
+    ///     // Put the object into the store
+    ///     store
+    ///         .put(&location, content.as_ref().into())
+    ///         .await
+    ///         .expect("Failed to put object");
+    ///
+    ///     // Get the object from the store to figure out the right etag
+    ///     let result: object_store::GetResult = 
store.get(&location).await.expect("Failed to get object");
+    ///
+    ///     let etag = result.meta.e_tag.expect("ETag should be present");
+    ///
+    ///     // Get the object from the store with range and etag
+    ///     let bytes = store
+    ///         .get_opts(
+    ///             &location,
+    ///             GetOptions::new()
+    ///                 .with_range(Some(0..5))
+    ///                 .with_if_match(Some(etag.clone())),
+    ///         )
+    ///         .await
+    ///         .expect("Failed to get object with range and etag")
+    ///         .bytes()
+    ///         .await
+    ///         .expect("Failed to read bytes");
+    ///
+    ///     println!(
+    ///         "Retrieved range [0-5] with ETag {}: {}",
+    ///         etag,
+    ///         String::from_utf8_lossy(&bytes)
+    ///     );
+    ///
+    ///     // Show that if the etag does not match, we get an error
+    ///     let wrong_etag = "wrong-etag".to_string();
+    ///     match store
+    ///         .get_opts(
+    ///             &location,
+    ///             
GetOptions::new().with_range(Some(0..5)).with_if_match(Some(wrong_etag))
+    ///         )
+    ///         .await
+    ///     {
+    ///         Ok(_) => println!("Unexpectedly succeeded with wrong ETag"),
+    ///         Err(e) => println!("On a non-versioned object store, getting 
an invalid ETag ('wrong-etag') results in an error as expected: {}", e),
+    ///     }
+    /// }
+    /// ```
     async fn get_opts(&self, location: &Path, options: GetOptions) -> 
Result<GetResult>;
 
     /// Return the bytes that are stored at the specified location
     /// in the given byte range.
     ///
-    /// See [`GetRange::Bounded`] for more details on how `range` gets 
interpreted
+    /// See [`GetRange::Bounded`] for more details on how `range` gets 
interpreted.
+    ///
+    /// To retrieve a range of bytes from a versioned object, use 
[`ObjectStore::get_opts`] by specifying the range in the [`GetOptions`].
+    ///
+    /// ## Examples
+    ///
+    /// This example uses a basic local filesystem object store to get a byte 
range from an object.
+    ///
+    /// ```ignore-wasm32
+    /// # use object_store::local::LocalFileSystem;
+    /// # use tempfile::tempdir;
+    /// # use object_store::{path::Path, ObjectStore};
+    /// async fn get_range_example() {
+    ///     let tmp = tempdir().unwrap();
+    ///     let store = LocalFileSystem::new_with_prefix(tmp.path()).unwrap();
+    ///     let location = Path::from("example.txt");
+    ///     let content = b"Hello, Object Store!";
+    ///
+    ///     // Put the object into the store
+    ///     store
+    ///         .put(&location, content.as_ref().into())
+    ///         .await
+    ///         .expect("Failed to put object");
+    ///
+    ///     // Get the object from the store
+    ///     let bytes = store
+    ///         .get_range(&location, 0..5)
+    ///         .await
+    ///         .expect("Failed to get object");
+    ///     println!("Retrieved range [0-5]: {}", 
String::from_utf8_lossy(&bytes));
+    /// }
+    /// ```
     async fn get_range(&self, location: &Path, range: Range<u64>) -> 
Result<Bytes> {
-        let options = GetOptions {
-            range: Some(range.into()),
-            ..Default::default()
-        };
+        let options = GetOptions::new().with_range(Some(range));
         self.get_opts(location, options).await?.bytes().await
     }
 
@@ -666,10 +863,7 @@ pub trait ObjectStore: std::fmt::Display + Send + Sync + 
Debug + 'static {
 
     /// Return the metadata for the specified location
     async fn head(&self, location: &Path) -> Result<ObjectMeta> {
-        let options = GetOptions {
-            head: true,
-            ..Default::default()
-        };
+        let options = GetOptions::new().with_head(true);
         Ok(self.get_opts(location, options).await?.meta)
     }
 
@@ -1035,6 +1229,83 @@ impl GetOptions {
         }
         Ok(())
     }
+
+    /// Create a new [`GetOptions`]
+    pub fn new() -> Self {
+        Self::default()
+    }
+
+    /// Sets the `if_match` condition.
+    ///
+    /// See [`GetOptions::if_match`]
+    #[must_use]
+    pub fn with_if_match(mut self, etag: Option<impl Into<String>>) -> Self {
+        self.if_match = etag.map(Into::into);
+        self
+    }
+
+    /// Sets the `if_none_match` condition.
+    ///
+    /// See [`GetOptions::if_none_match`]
+    #[must_use]
+    pub fn with_if_none_match(mut self, etag: Option<impl Into<String>>) -> 
Self {
+        self.if_none_match = etag.map(Into::into);
+        self
+    }
+
+    /// Sets the `if_modified_since` condition.
+    ///
+    /// See [`GetOptions::if_modified_since`]
+    #[must_use]
+    pub fn with_if_modified_since(mut self, dt: Option<impl 
Into<DateTime<Utc>>>) -> Self {
+        self.if_modified_since = dt.map(Into::into);
+        self
+    }
+
+    /// Sets the `if_unmodified_since` condition.
+    ///
+    /// See [`GetOptions::if_unmodified_since`]
+    #[must_use]
+    pub fn with_if_unmodified_since(mut self, dt: Option<impl 
Into<DateTime<Utc>>>) -> Self {
+        self.if_unmodified_since = dt.map(Into::into);
+        self
+    }
+
+    /// Sets the `range` condition.
+    ///
+    /// See [`GetOptions::range`]
+    #[must_use]
+    pub fn with_range(mut self, range: Option<impl Into<GetRange>>) -> Self {
+        self.range = range.map(Into::into);
+        self
+    }
+
+    /// Sets the `version` condition.
+    ///
+    /// See [`GetOptions::version`]
+    #[must_use]
+    pub fn with_version(mut self, version: Option<impl Into<String>>) -> Self {
+        self.version = version.map(Into::into);
+        self
+    }
+
+    /// Sets the `head` condition.
+    ///
+    /// See [`GetOptions::head`]
+    #[must_use]
+    pub fn with_head(mut self, head: impl Into<bool>) -> Self {
+        self.head = head.into();
+        self
+    }
+
+    /// Sets the `extensions` condition.
+    ///
+    /// See [`GetOptions::extensions`]
+    #[must_use]
+    pub fn with_extensions(mut self, extensions: Extensions) -> Self {
+        self.extensions = extensions;
+        self
+    }
 }
 
 /// Result for a get request
@@ -1670,4 +1941,41 @@ mod tests {
         extensions.insert("test-key");
         assert!(extensions.get::<&str>().is_some());
     }
+
+    #[test]
+    fn test_get_options_builder() {
+        let dt = Utc::now();
+        let extensions = Extensions::new();
+
+        let options = GetOptions::new();
+
+        // assert defaults
+        assert_eq!(options.if_match, None);
+        assert_eq!(options.if_none_match, None);
+        assert_eq!(options.if_modified_since, None);
+        assert_eq!(options.if_unmodified_since, None);
+        assert_eq!(options.range, None);
+        assert_eq!(options.version, None);
+        assert!(!options.head);
+        assert!(options.extensions.get::<&str>().is_none());
+
+        let options = options
+            .with_if_match(Some("etag-match"))
+            .with_if_none_match(Some("etag-none-match"))
+            .with_if_modified_since(Some(dt))
+            .with_if_unmodified_since(Some(dt))
+            .with_range(Some(0..100))
+            .with_version(Some("version-1"))
+            .with_head(true)
+            .with_extensions(extensions.clone());
+
+        assert_eq!(options.if_match, Some("etag-match".to_string()));
+        assert_eq!(options.if_none_match, Some("etag-none-match".to_string()));
+        assert_eq!(options.if_modified_since, Some(dt));
+        assert_eq!(options.if_unmodified_since, Some(dt));
+        assert_eq!(options.range, Some(GetRange::Bounded(0..100)));
+        assert_eq!(options.version, Some("version-1".to_string()));
+        assert!(options.head);
+        assert_eq!(options.extensions.get::<&str>(), extensions.get::<&str>());
+    }
 }
diff --git a/tests/get_range_file.rs b/tests/get_range_file.rs
index d5ac8e3..04317af 100644
--- a/tests/get_range_file.rs
+++ b/tests/get_range_file.rs
@@ -115,11 +115,36 @@ async fn test_get_opts_over_range() {
     let expected = Bytes::from_static(b"hello world");
     store.put(&path, expected.clone().into()).await.unwrap();
 
-    let opts = GetOptions {
-        range: Some(GetRange::Bounded(0..(expected.len() as u64 * 2))),
-        ..Default::default()
-    };
+    let opts =
+        GetOptions::new().with_range(Some(GetRange::Bounded(0..(expected.len() 
as u64 * 2))));
     let res = store.get_opts(&path, opts).await.unwrap();
     assert_eq!(res.range, 0..expected.len() as u64);
     assert_eq!(res.bytes().await.unwrap(), expected);
 }
+
+#[tokio::test]
+async fn test_get_range_opts_with_etag() {
+    let tmp = tempdir().unwrap();
+    let store = MyStore(LocalFileSystem::new_with_prefix(tmp.path()).unwrap());
+    let path = Path::from("foo");
+
+    let expected = Bytes::from_static(b"hello world");
+    store.put(&path, expected.clone().into()).await.unwrap();
+
+    // pull the file to get the etag
+    let file = store.get(&path).await.unwrap();
+    let etag = file.meta.e_tag.clone().unwrap();
+
+    let opts = GetOptions::new()
+        .with_if_match(Some(etag))
+        .with_range(Some(0..(expected.len() as u64 * 2)));
+    let res = store.get_opts(&path, opts).await.unwrap();
+    assert_eq!(res.bytes().await.unwrap(), expected);
+
+    // pulling a file with an invalid etag should fail
+    let opts = GetOptions::new()
+        .with_if_match(Some("invalid-etag"))
+        .with_range(Some(0..(expected.len() as u64 * 2)));
+    let err = store.get_opts(&path, opts).await;
+    assert!(err.is_err());
+}
diff --git a/tests/http.rs b/tests/http.rs
index cb0b7d6..6c41792 100644
--- a/tests/http.rs
+++ b/tests/http.rs
@@ -36,10 +36,7 @@ async fn test_http_store_gzip() {
     let _ = http_store
         .get_opts(
             &Path::parse("LICENSE.txt").unwrap(),
-            GetOptions {
-                range: Some(GetRange::Bounded(0..100)),
-                ..Default::default()
-            },
+            GetOptions::new().with_range(Some(GetRange::Bounded(0..100))),
         )
         .await
         .unwrap();
@@ -56,10 +53,7 @@ async fn basic_wasm_get() {
     let _ = http_store
         .get_opts(
             &Path::parse("LICENSE.txt").unwrap(),
-            GetOptions {
-                range: Some(GetRange::Bounded(0..100)),
-                ..Default::default()
-            },
+            GetOptions::new().with_range(Some(GetRange::Bounded(0..100))),
         )
         .await
         .unwrap();

Reply via email to