From d1214c7679fc63c2931063efd7818dd2f39361c0 Mon Sep 17 00:00:00 2001 From: Jan Michael Auer Date: Tue, 23 Jun 2026 16:10:26 +0200 Subject: [PATCH] feat(client): Accept optional tokens in ClientBuilder::token Callers that hold an optional token previously had to guard the builder call, conditionally invoking `.token()` only when a value was present. Introduce an `IntoTokenProvider` trait, implemented for `TokenGenerator`, `String`, `&str`, and any `Option` of those. `ClientBuilder::token` now accepts an `Option` directly, leaving the client unauthenticated on `None`, so optional authentication needs no surrounding conditional. --- clients/rust/README.md | 1 + clients/rust/src/auth.rs | 48 ++++++++++++++++++++++++++++---------- clients/rust/src/client.rs | 26 ++++++++++++++++++--- stresstest/src/http.rs | 12 ++++------ 4 files changed, 65 insertions(+), 22 deletions(-) diff --git a/clients/rust/README.md b/clients/rust/README.md index 9f801990..4b8a6335 100644 --- a/clients/rust/README.md +++ b/clients/rust/README.md @@ -286,6 +286,7 @@ via [`ClientBuilder::token`]. It accepts either: and scope being accessed. - A **`String` / `&str`** — a pre-signed JWT, used as-is for every request. Use this for external services that receive a token from another source. +- An `Option` of any of the above — useful for chained builder calls. ```rust,ignore use objectstore_client::{Client, SecretKey, TokenGenerator, Usecase}; diff --git a/clients/rust/src/auth.rs b/clients/rust/src/auth.rs index d03c09c0..ae043ebf 100644 --- a/clients/rust/src/auth.rs +++ b/clients/rust/src/auth.rs @@ -55,21 +55,44 @@ impl std::fmt::Debug for TokenProvider { } } -impl From for TokenProvider { - fn from(generator: TokenGenerator) -> Self { - TokenProvider::Generator(generator) +/// Conversion into an optional [`TokenProvider`] for [`ClientBuilder::token`]. +/// +/// This is implemented for [`TokenGenerator`], `String`, and `&str`, each of which yields a +/// configured provider. It is also implemented for any `Option` where `T: IntoTokenProvider`, +/// so a `None` resolves to no authentication and a `Some(value)` to the inner provider. This lets +/// callers pass optional auth configuration to [`ClientBuilder::token`] without an explicit +/// conditional. +/// +/// [`ClientBuilder::token`]: crate::ClientBuilder::token +pub trait IntoTokenProvider { + /// Converts `self` into an optional [`TokenProvider`]. + fn into_token_provider(self) -> Option; +} + +impl IntoTokenProvider for Option +where + T: IntoTokenProvider, +{ + fn into_token_provider(self) -> Option { + self.and_then(|t| t.into_token_provider()) + } +} + +impl IntoTokenProvider for TokenGenerator { + fn into_token_provider(self) -> Option { + Some(TokenProvider::Generator(self)) } } -impl From for TokenProvider { - fn from(token: String) -> Self { - TokenProvider::Static(token) +impl IntoTokenProvider for String { + fn into_token_provider(self) -> Option { + Some(TokenProvider::Static(self)) } } -impl From<&str> for TokenProvider { - fn from(token: &str) -> Self { - TokenProvider::Static(token.to_owned()) +impl IntoTokenProvider for &str { + fn into_token_provider(self) -> Option { + Some(TokenProvider::Static(self.to_owned())) } } @@ -78,9 +101,10 @@ impl From<&str> for TokenProvider { /// Tokens are signed with an EdDSA private key and have certain permissions and expiry timeouts /// applied. /// -/// Use this for internal services that have access to an EdDSA keypair. You can pass a -/// `TokenGenerator` directly to [`ClientBuilder::token`](crate::ClientBuilder::token), -/// and it will be automatically converted into a [`TokenProvider::Generator`]. +/// Use this for internal services that have access to an EdDSA keypair. A `TokenGenerator` +/// implements [`IntoTokenProvider`], so it can be passed directly to +/// [`ClientBuilder::token`](crate::ClientBuilder::token), where it becomes a +/// [`TokenProvider::Generator`]. #[derive(Debug)] pub struct TokenGenerator { kid: String, diff --git a/clients/rust/src/client.rs b/clients/rust/src/client.rs index 727f2068..3f34785d 100644 --- a/clients/rust/src/client.rs +++ b/clients/rust/src/client.rs @@ -9,6 +9,7 @@ use objectstore_types::scope; use reqwest::RequestBuilder; use url::Url; +use crate::IntoTokenProvider; use crate::auth::TokenProvider; const USER_AGENT: &str = concat!("objectstore-client/", env!("CARGO_PKG_VERSION")); @@ -110,13 +111,15 @@ impl ClientBuilder { /// Sets the authentication token to use for requests to Objectstore. /// - /// Accepts anything that implements `Into`: + /// Accepts anything that implements [`IntoTokenProvider`]: /// - A [`TokenGenerator`](crate::TokenGenerator) — for internal services that have access to /// an EdDSA keypair. The generator signs a fresh JWT for each request. /// - A `String` or `&str` — a pre-signed JWT, used as-is for every request. - pub fn token(self, token: impl Into) -> Self { + /// - An `Option` of any of the above — a `None` leaves the client unauthenticated, which is + /// convenient when authentication is configured conditionally. + pub fn token(self, token: impl IntoTokenProvider) -> Self { let Ok(mut inner) = self.0 else { return self }; - inner.token = Some(token.into()); + inner.token = token.into_token_provider(); Self(Ok(inner)) } @@ -365,6 +368,23 @@ pub(crate) struct ClientInner { /// # Ok(()) /// # } /// ``` +/// +/// Optional authentication — pass an `Option` straight through, leaving the client +/// unauthenticated when it is `None`: +/// +/// ```no_run +/// use objectstore_client::Client; +/// +/// # fn example() -> objectstore_client::Result<()> { +/// // Authenticate only if a token is present in the environment. +/// let token_opt = std::env::var("OBJECTSTORE_TOKEN").ok(); +/// +/// let client = Client::builder("http://localhost:8888/") +/// .token(token_opt) +/// .build()?; +/// # Ok(()) +/// # } +/// ``` #[derive(Debug, Clone)] pub struct Client { inner: Arc, diff --git a/stresstest/src/http.rs b/stresstest/src/http.rs index 5deb4f5a..6f2f8d1e 100644 --- a/stresstest/src/http.rs +++ b/stresstest/src/http.rs @@ -18,14 +18,12 @@ pub struct HttpRemote { impl HttpRemote { /// Creates a new `HttpRemote` instance with the given remote URL and optional token generator. pub fn new(remote: &str, token: Option) -> Self { - let mut builder = Client::builder(remote).configure_reqwest(|r| r.no_hickory_dns()); - if let Some(t) = token { - builder = builder.token(t); - } Self { - // INVARIANT: builder is always valid — remote is a caller-supplied URL and no - // fallible configuration is applied. - client: builder.build().unwrap(), + client: Client::builder(remote) + .configure_reqwest(|r| r.no_hickory_dns()) + .token(token) + .build() + .unwrap(), } }