google_cloud_storage/storage/client.rs
1// Copyright 2025 Google LLC
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7// https://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15use super::request_options::RequestOptions;
16use crate::Error;
17use crate::builder::storage::ReadObject;
18use crate::builder::storage::WriteObject;
19use crate::read_resume_policy::ReadResumePolicy;
20use crate::streaming_source::Payload;
21use auth::credentials::CacheableResource;
22use base64::Engine;
23use base64::prelude::BASE64_STANDARD;
24use http::Extensions;
25use std::sync::Arc;
26
27/// Implements a client for the Cloud Storage API.
28///
29/// # Example
30/// ```
31/// # async fn sample() -> anyhow::Result<()> {
32/// # use google_cloud_storage::client::Storage;
33/// let client = Storage::builder().build().await?;
34/// // use `client` to make requests to Cloud Storage.
35/// # Ok(()) }
36/// ```
37///
38/// # Configuration
39///
40/// To configure `Storage` use the `with_*` methods in the type returned
41/// by [builder()][Storage::builder]. The default configuration should
42/// work for most applications. Common configuration changes include
43///
44/// * [with_endpoint()]: by default this client uses the global default endpoint
45/// (`https://storage.googleapis.com`). Applications using regional
46/// endpoints or running in restricted networks (e.g. a network configured
47/// with [Private Google Access with VPC Service Controls]) may want to
48/// override this default.
49/// * [with_credentials()]: by default this client uses
50/// [Application Default Credentials]. Applications using custom
51/// authentication may need to override this default.
52///
53/// # Pooling and Cloning
54///
55/// `Storage` holds a connection pool internally, it is advised to
56/// create one and then reuse it. You do not need to wrap `Storage` in
57/// an [Rc](std::rc::Rc) or [Arc] to reuse it, because it already uses an `Arc`
58/// internally.
59///
60/// # Service Description
61///
62/// The Cloud Storage API allows applications to read and write data through
63/// the abstractions of buckets and objects. For a description of these
64/// abstractions please see <https://cloud.google.com/storage/docs>.
65///
66/// Resources are named as follows:
67///
68/// - Projects are referred to as they are defined by the Resource Manager API,
69/// using strings like `projects/123456` or `projects/my-string-id`.
70///
71/// - Buckets are named using string names of the form:
72/// `projects/{project}/buckets/{bucket}`
73/// For globally unique buckets, `_` may be substituted for the project.
74///
75/// - Objects are uniquely identified by their name along with the name of the
76/// bucket they belong to, as separate strings in this API. For example:
77/// ```no_rust
78/// bucket = "projects/_/buckets/my-bucket"
79/// object = "my-object/with/a/folder-like/name"
80/// ```
81/// Note that object names can contain `/` characters, which are treated as
82/// any other character (no special directory semantics).
83///
84/// [with_endpoint()]: ClientBuilder::with_endpoint
85/// [with_credentials()]: ClientBuilder::with_credentials
86/// [Private Google Access with VPC Service Controls]: https://cloud.google.com/vpc-service-controls/docs/private-connectivity
87/// [Application Default Credentials]: https://cloud.google.com/docs/authentication#adc
88#[derive(Clone, Debug)]
89pub struct Storage<S = crate::storage::transport::Storage>
90where
91 S: crate::storage::stub::Storage + 'static,
92{
93 stub: std::sync::Arc<S>,
94 options: RequestOptions,
95}
96
97#[derive(Clone, Debug)]
98pub(crate) struct StorageInner {
99 pub client: reqwest::Client,
100 pub cred: auth::credentials::Credentials,
101 pub endpoint: String,
102 pub options: RequestOptions,
103}
104
105impl Storage {
106 /// Returns a builder for [Storage].
107 ///
108 /// # Example
109 /// ```
110 /// # use google_cloud_storage::client::Storage;
111 /// # async fn sample() -> anyhow::Result<()> {
112 /// let client = Storage::builder().build().await?;
113 /// # Ok(()) }
114 /// ```
115 pub fn builder() -> ClientBuilder {
116 ClientBuilder::new()
117 }
118}
119
120impl<S> Storage<S>
121where
122 S: crate::storage::stub::Storage + 'static,
123{
124 /// Creates a new client from the provided stub.
125 ///
126 /// The most common case for calling this function is in tests mocking the
127 /// client's behavior.
128 pub fn from_stub(stub: S) -> Self
129 where
130 S: super::stub::Storage + 'static,
131 {
132 Self {
133 stub: std::sync::Arc::new(stub),
134 options: RequestOptions::new(),
135 }
136 }
137
138 /// Write an object with data from any data source.
139 ///
140 /// # Example
141 /// ```
142 /// # use google_cloud_storage::client::Storage;
143 /// # async fn sample(client: &Storage) -> anyhow::Result<()> {
144 /// let response = client
145 /// .write_object("projects/_/buckets/my-bucket", "my-object", "hello world")
146 /// .send_buffered()
147 /// .await?;
148 /// println!("response details={response:?}");
149 /// # Ok(()) }
150 /// ```
151 ///
152 /// # Example
153 /// ```
154 /// # use google_cloud_storage::client::Storage;
155 /// # async fn sample(client: &Storage) -> anyhow::Result<()> {
156 /// let response = client
157 /// .write_object("projects/_/buckets/my-bucket", "my-object", "hello world")
158 /// .send_unbuffered()
159 /// .await?;
160 /// println!("response details={response:?}");
161 /// # Ok(()) }
162 /// ```
163 ///
164 /// You can use many different types as the payload. For example, a string,
165 /// a [bytes::Bytes], a [tokio::fs::File], or a custom type that implements
166 /// the [StreamingSource] trait.
167 ///
168 /// If your data source also implements [Seek], prefer [send_unbuffered()]
169 /// to start the write. Otherwise use [send_buffered()].
170 ///
171 /// # Parameters
172 /// * `bucket` - the bucket name containing the object. In
173 /// `projects/_/buckets/{bucket_id}` format.
174 /// * `object` - the object name.
175 /// * `payload` - the object data.
176 ///
177 /// [Seek]: crate::streaming_source::Seek
178 /// [StreamingSource]: crate::streaming_source::StreamingSource
179 /// [send_buffered()]: crate::builder::storage::WriteObject::send_buffered
180 /// [send_unbuffered()]: crate::builder::storage::WriteObject::send_unbuffered
181 pub fn write_object<B, O, T, P>(&self, bucket: B, object: O, payload: T) -> WriteObject<P, S>
182 where
183 B: Into<String>,
184 O: Into<String>,
185 T: Into<Payload<P>>,
186 {
187 WriteObject::new(
188 self.stub.clone(),
189 bucket,
190 object,
191 payload,
192 self.options.clone(),
193 )
194 }
195
196 /// Reads the contents of an object.
197 ///
198 /// # Example
199 /// ```
200 /// # use google_cloud_storage::client::Storage;
201 /// # async fn sample(client: &Storage) -> anyhow::Result<()> {
202 /// let mut resp = client
203 /// .read_object("projects/_/buckets/my-bucket", "my-object")
204 /// .send()
205 /// .await?;
206 /// let mut contents = Vec::new();
207 /// while let Some(chunk) = resp.next().await.transpose()? {
208 /// contents.extend_from_slice(&chunk);
209 /// }
210 /// println!("object contents={:?}", bytes::Bytes::from_owner(contents));
211 /// # Ok(()) }
212 /// ```
213 ///
214 /// # Parameters
215 /// * `bucket` - the bucket name containing the object. In
216 /// `projects/_/buckets/{bucket_id}` format.
217 /// * `object` - the object name.
218 pub fn read_object<B, O>(&self, bucket: B, object: O) -> ReadObject<S>
219 where
220 B: Into<String>,
221 O: Into<String>,
222 {
223 ReadObject::new(self.stub.clone(), bucket, object, self.options.clone())
224 }
225}
226
227impl Storage {
228 pub(crate) fn new(builder: ClientBuilder) -> gax::client_builder::Result<Self> {
229 use gax::client_builder::Error;
230 let client = reqwest::Client::builder()
231 // Disable all automatic decompression. These could be enabled by users by enabling
232 // the corresponding features flags, but we will not be able to tell whether this
233 // has happened.
234 .no_brotli()
235 .no_deflate()
236 .no_gzip()
237 .no_zstd()
238 .build()
239 .map_err(Error::transport)?;
240 let mut builder = builder;
241 let cred = if let Some(c) = builder.credentials {
242 c
243 } else {
244 auth::credentials::Builder::default()
245 .build()
246 .map_err(Error::cred)?
247 };
248 let endpoint = builder
249 .endpoint
250 .unwrap_or_else(|| self::DEFAULT_HOST.to_string());
251 builder.credentials = Some(cred);
252 builder.endpoint = Some(endpoint);
253 let inner = Arc::new(StorageInner::new(client, builder));
254 let options = inner.options.clone();
255 let stub = crate::storage::transport::Storage::new(inner);
256 Ok(Self { stub, options })
257 }
258}
259
260impl StorageInner {
261 /// Builds a client assuming `config.cred` and `config.endpoint` are initialized, panics otherwise.
262 pub(self) fn new(client: reqwest::Client, builder: ClientBuilder) -> Self {
263 Self {
264 client,
265 cred: builder
266 .credentials
267 .expect("StorageInner assumes the credentials are initialized"),
268 endpoint: builder
269 .endpoint
270 .expect("StorageInner assumes the endpoint is initialized"),
271 options: builder.default_options,
272 }
273 }
274
275 // Helper method to apply authentication headers to the request builder.
276 pub async fn apply_auth_headers(
277 &self,
278 builder: reqwest::RequestBuilder,
279 ) -> crate::Result<reqwest::RequestBuilder> {
280 let cached_auth_headers = self
281 .cred
282 .headers(Extensions::new())
283 .await
284 .map_err(Error::authentication)?;
285
286 let auth_headers = match cached_auth_headers {
287 CacheableResource::New { data, .. } => data,
288 CacheableResource::NotModified => {
289 unreachable!("headers are not cached");
290 }
291 };
292
293 let builder = builder.headers(auth_headers);
294 Ok(builder)
295 }
296}
297
298/// A builder for [Storage].
299///
300/// ```
301/// # use google_cloud_storage::client::Storage;
302/// # async fn sample() -> anyhow::Result<()> {
303/// let builder = Storage::builder();
304/// let client = builder
305/// .with_endpoint("https://storage.googleapis.com")
306/// .build()
307/// .await?;
308/// # Ok(()) }
309/// ```
310pub struct ClientBuilder {
311 pub(crate) endpoint: Option<String>,
312 pub(crate) credentials: Option<auth::credentials::Credentials>,
313 // Default options for requests.
314 pub(crate) default_options: RequestOptions,
315}
316
317impl ClientBuilder {
318 pub(crate) fn new() -> Self {
319 Self {
320 endpoint: None,
321 credentials: None,
322 default_options: RequestOptions::new(),
323 }
324 }
325
326 /// Creates a new client.
327 ///
328 /// # Example
329 /// ```
330 /// # use google_cloud_storage::client::Storage;
331 /// # async fn sample() -> anyhow::Result<()> {
332 /// let client = Storage::builder().build().await?;
333 /// # Ok(()) }
334 /// ```
335 pub async fn build(self) -> gax::client_builder::Result<Storage> {
336 Storage::new(self)
337 }
338
339 /// Sets the endpoint.
340 ///
341 /// # Example
342 /// ```
343 /// # use google_cloud_storage::client::Storage;
344 /// # async fn sample() -> anyhow::Result<()> {
345 /// let client = Storage::builder()
346 /// .with_endpoint("https://private.googleapis.com")
347 /// .build()
348 /// .await?;
349 /// # Ok(()) }
350 /// ```
351 pub fn with_endpoint<V: Into<String>>(mut self, v: V) -> Self {
352 self.endpoint = Some(v.into());
353 self
354 }
355
356 /// Configures the authentication credentials.
357 ///
358 /// Google Cloud Storage requires authentication for most buckets. Use this
359 /// method to change the credentials used by the client. More information
360 /// about valid credentials types can be found in the [google-cloud-auth]
361 /// crate documentation.
362 ///
363 /// # Example
364 /// ```
365 /// # use google_cloud_storage::client::Storage;
366 /// # async fn sample() -> anyhow::Result<()> {
367 /// use auth::credentials::mds;
368 /// let client = Storage::builder()
369 /// .with_credentials(
370 /// mds::Builder::default()
371 /// .with_scopes(["https://www.googleapis.com/auth/cloud-platform.read-only"])
372 /// .build()?)
373 /// .build()
374 /// .await?;
375 /// # Ok(()) }
376 /// ```
377 ///
378 /// [google-cloud-auth]: https://docs.rs/google-cloud-auth
379 pub fn with_credentials<V: Into<auth::credentials::Credentials>>(mut self, v: V) -> Self {
380 self.credentials = Some(v.into());
381 self
382 }
383
384 /// Configure the retry policy.
385 ///
386 /// The client libraries can automatically retry operations that fail. The
387 /// retry policy controls what errors are considered retryable, sets limits
388 /// on the number of attempts or the time trying to make attempts.
389 ///
390 /// # Example
391 /// ```
392 /// # use google_cloud_storage::client::Storage;
393 /// # async fn sample() -> anyhow::Result<()> {
394 /// use gax::retry_policy::{AlwaysRetry, RetryPolicyExt};
395 /// let client = Storage::builder()
396 /// .with_retry_policy(AlwaysRetry.with_attempt_limit(3))
397 /// .build()
398 /// .await?;
399 /// # Ok(()) }
400 /// ```
401 pub fn with_retry_policy<V: Into<gax::retry_policy::RetryPolicyArg>>(mut self, v: V) -> Self {
402 self.default_options.retry_policy = v.into().into();
403 self
404 }
405
406 /// Configure the retry backoff policy.
407 ///
408 /// The client libraries can automatically retry operations that fail. The
409 /// backoff policy controls how long to wait in between retry attempts.
410 ///
411 /// # Example
412 /// ```
413 /// # use google_cloud_storage::client::Storage;
414 /// # async fn sample() -> anyhow::Result<()> {
415 /// use gax::exponential_backoff::ExponentialBackoff;
416 /// use std::time::Duration;
417 /// let policy = ExponentialBackoff::default();
418 /// let client = Storage::builder()
419 /// .with_backoff_policy(policy)
420 /// .build()
421 /// .await?;
422 /// # Ok(()) }
423 /// ```
424 pub fn with_backoff_policy<V: Into<gax::backoff_policy::BackoffPolicyArg>>(
425 mut self,
426 v: V,
427 ) -> Self {
428 self.default_options.backoff_policy = v.into().into();
429 self
430 }
431
432 /// Configure the retry throttler.
433 ///
434 /// Advanced applications may want to configure a retry throttler to
435 /// [Address Cascading Failures] and when [Handling Overload] conditions.
436 /// The client libraries throttle their retry loop, using a policy to
437 /// control the throttling algorithm. Use this method to fine tune or
438 /// customize the default retry throtler.
439 ///
440 /// [Handling Overload]: https://sre.google/sre-book/handling-overload/
441 /// [Addressing Cascading Failures]: https://sre.google/sre-book/addressing-cascading-failures/
442 ///
443 /// # Example
444 /// ```
445 /// # use google_cloud_storage::client::Storage;
446 /// # async fn sample() -> anyhow::Result<()> {
447 /// use gax::retry_throttler::AdaptiveThrottler;
448 /// let client = Storage::builder()
449 /// .with_retry_throttler(AdaptiveThrottler::default())
450 /// .build()
451 /// .await?;
452 /// # Ok(()) }
453 /// ```
454 pub fn with_retry_throttler<V: Into<gax::retry_throttler::RetryThrottlerArg>>(
455 mut self,
456 v: V,
457 ) -> Self {
458 self.default_options.retry_throttler = v.into().into();
459 self
460 }
461
462 /// Sets the payload size threshold to switch from single-shot to resumable uploads.
463 ///
464 /// # Example
465 /// ```
466 /// # use google_cloud_storage::client::Storage;
467 /// # async fn sample() -> anyhow::Result<()> {
468 /// let client = Storage::builder()
469 /// .with_resumable_upload_threshold(0_usize) // Forces a resumable upload.
470 /// .build()
471 /// .await?;
472 /// let response = client
473 /// .write_object("projects/_/buckets/my-bucket", "my-object", "hello world")
474 /// .send_buffered()
475 /// .await?;
476 /// println!("response details={response:?}");
477 /// # Ok(()) }
478 /// ```
479 ///
480 /// The client library can write objects using [single-shot] or [resumable]
481 /// uploads. For small objects, single-shot uploads offer better
482 /// performance, as they require a single HTTP transfer. For larger objects,
483 /// the additional request latency is not significant, and resumable uploads
484 /// offer better recovery on errors.
485 ///
486 /// The library automatically selects resumable uploads when the payload is
487 /// equal to or larger than this option. For smaller writes the client
488 /// library uses single-shot uploads.
489 ///
490 /// The exact threshold depends on where the application is deployed and
491 /// destination bucket location with respect to where the application is
492 /// running. The library defaults should work well in most cases, but some
493 /// applications may benefit from fine-tuning.
494 ///
495 /// [single-shot]: https://cloud.google.com/storage/docs/uploading-objects
496 /// [resumable]: https://cloud.google.com/storage/docs/resumable-uploads
497 pub fn with_resumable_upload_threshold<V: Into<usize>>(mut self, v: V) -> Self {
498 self.default_options.resumable_upload_threshold = v.into();
499 self
500 }
501
502 /// Changes the buffer size for some resumable uploads.
503 ///
504 /// # Example
505 /// ```
506 /// # use google_cloud_storage::client::Storage;
507 /// # async fn sample() -> anyhow::Result<()> {
508 /// let client = Storage::builder()
509 /// .with_resumable_upload_buffer_size(32 * 1024 * 1024_usize)
510 /// .build()
511 /// .await?;
512 /// let response = client
513 /// .write_object("projects/_/buckets/my-bucket", "my-object", "hello world")
514 /// .send_buffered()
515 /// .await?;
516 /// println!("response details={response:?}");
517 /// # Ok(()) }
518 /// ```
519 ///
520 /// When performing [resumable uploads] from sources without [Seek] the
521 /// client library needs to buffer data in memory until it is persisted by
522 /// the service. Otherwise the data would be lost if the upload is
523 /// interrupted. Applications may want to tune this buffer size:
524 ///
525 /// - Use smaller buffer sizes to support more concurrent writes in the
526 /// same application.
527 /// - Use larger buffer sizes for better throughput. Sending many small
528 /// buffers stalls the writer until the client receives a successful
529 /// response from the service.
530 ///
531 /// Keep in mind that there are diminishing returns on using larger buffers.
532 ///
533 /// [resumable uploads]: https://cloud.google.com/storage/docs/resumable-uploads
534 /// [Seek]: crate::streaming_source::Seek
535 pub fn with_resumable_upload_buffer_size<V: Into<usize>>(mut self, v: V) -> Self {
536 self.default_options.resumable_upload_buffer_size = v.into();
537 self
538 }
539
540 /// Configure the resume policy for object reads.
541 ///
542 /// The Cloud Storage client library can automatically resume a read request
543 /// that is interrupted by a transient error. Applications may want to
544 /// limit the number of read attempts, or may wish to expand the type
545 /// of errors treated as retryable.
546 ///
547 /// # Example
548 /// ```
549 /// # use google_cloud_storage::client::Storage;
550 /// # async fn sample() -> anyhow::Result<()> {
551 /// use google_cloud_storage::read_resume_policy::{AlwaysResume, ReadResumePolicyExt};
552 /// let client = Storage::builder()
553 /// .with_read_resume_policy(AlwaysResume.with_attempt_limit(3))
554 /// .build()
555 /// .await?;
556 /// # Ok(()) }
557 /// ```
558 pub fn with_read_resume_policy<V>(mut self, v: V) -> Self
559 where
560 V: ReadResumePolicy + 'static,
561 {
562 self.default_options.read_resume_policy = Arc::new(v);
563 self
564 }
565}
566
567/// The default host used by the service.
568const DEFAULT_HOST: &str = "https://storage.googleapis.com";
569
570pub(crate) mod info {
571 const NAME: &str = env!("CARGO_PKG_NAME");
572 const VERSION: &str = env!("CARGO_PKG_VERSION");
573 lazy_static::lazy_static! {
574 pub(crate) static ref X_GOOG_API_CLIENT_HEADER: String = {
575 let ac = gaxi::api_header::XGoogApiClient{
576 name: NAME,
577 version: VERSION,
578 library_type: gaxi::api_header::GCCL,
579 };
580 ac.grpc_header_value()
581 };
582 }
583}
584
585/// The set of characters that are percent encoded.
586///
587/// This set is defined at https://cloud.google.com/storage/docs/request-endpoints#encoding:
588///
589/// Encode the following characters when they appear in either the object name
590/// or query string of a request URL:
591/// !, #, $, &, ', (, ), *, +, ,, /, :, ;, =, ?, @, [, ], and space characters.
592const ENCODED_CHARS: percent_encoding::AsciiSet = percent_encoding::CONTROLS
593 .add(b'!')
594 .add(b'#')
595 .add(b'$')
596 .add(b'&')
597 .add(b'\'')
598 .add(b'(')
599 .add(b')')
600 .add(b'*')
601 .add(b'+')
602 .add(b',')
603 .add(b'/')
604 .add(b':')
605 .add(b';')
606 .add(b'=')
607 .add(b'?')
608 .add(b'@')
609 .add(b'[')
610 .add(b']')
611 .add(b' ');
612
613/// Percent encode a string.
614///
615/// To ensure compatibility certain characters need to be encoded when they appear
616/// in either the object name or query string of a request URL.
617pub(crate) fn enc(value: &str) -> String {
618 percent_encoding::utf8_percent_encode(value, &ENCODED_CHARS).to_string()
619}
620
621pub(crate) fn apply_customer_supplied_encryption_headers(
622 builder: reqwest::RequestBuilder,
623 common_object_request_params: &Option<crate::model::CommonObjectRequestParams>,
624) -> reqwest::RequestBuilder {
625 common_object_request_params.iter().fold(builder, |b, v| {
626 b.header(
627 "x-goog-encryption-algorithm",
628 v.encryption_algorithm.clone(),
629 )
630 .header(
631 "x-goog-encryption-key",
632 BASE64_STANDARD.encode(v.encryption_key_bytes.clone()),
633 )
634 .header(
635 "x-goog-encryption-key-sha256",
636 BASE64_STANDARD.encode(v.encryption_key_sha256_bytes.clone()),
637 )
638 })
639}
640
641#[cfg(test)]
642pub(crate) mod tests {
643 use super::*;
644 use gax::retry_result::RetryResult;
645 use gax::retry_state::RetryState;
646 use std::{sync::Arc, time::Duration};
647
648 pub(crate) fn test_builder() -> ClientBuilder {
649 ClientBuilder::new()
650 .with_credentials(auth::credentials::anonymous::Builder::new().build())
651 .with_endpoint("http://private.googleapis.com")
652 .with_backoff_policy(
653 gax::exponential_backoff::ExponentialBackoffBuilder::new()
654 .with_initial_delay(Duration::from_millis(1))
655 .with_maximum_delay(Duration::from_millis(2))
656 .build()
657 .expect("hard coded policy should build correctly"),
658 )
659 }
660
661 /// This is used by the request builder tests.
662 pub(crate) fn test_inner_client(builder: ClientBuilder) -> Arc<StorageInner> {
663 let client = reqwest::Client::new();
664 Arc::new(StorageInner::new(client, builder))
665 }
666
667 mockall::mock! {
668 #[derive(Debug)]
669 pub RetryThrottler {}
670
671 impl gax::retry_throttler::RetryThrottler for RetryThrottler {
672 fn throttle_retry_attempt(&self) -> bool;
673 fn on_retry_failure(&mut self, flow: &RetryResult);
674 fn on_success(&mut self);
675 }
676 }
677
678 mockall::mock! {
679 #[derive(Debug)]
680 pub RetryPolicy {}
681
682 impl gax::retry_policy::RetryPolicy for RetryPolicy {
683 fn on_error(&self, state: &RetryState, error: gax::error::Error) -> RetryResult;
684 }
685 }
686
687 mockall::mock! {
688 #[derive(Debug)]
689 pub BackoffPolicy {}
690
691 impl gax::backoff_policy::BackoffPolicy for BackoffPolicy {
692 fn on_failure(&self, state: &RetryState) -> std::time::Duration;
693 }
694 }
695
696 mockall::mock! {
697 #[derive(Debug)]
698 pub ReadResumePolicy {}
699
700 impl crate::read_resume_policy::ReadResumePolicy for ReadResumePolicy {
701 fn on_error(&self, query: &crate::read_resume_policy::ResumeQuery, error: gax::error::Error) -> crate::read_resume_policy::ResumeResult;
702 }
703 }
704}