Skip to main content

s2_sdk/
types.rs

1//! Types relevant to [`S2`](crate::S2), [`S2Basin`](crate::S2Basin), and
2//! [`S2Stream`](crate::S2Stream).
3use std::{
4    collections::HashSet,
5    env::VarError,
6    fmt,
7    num::NonZeroU32,
8    ops::{Deref, RangeTo},
9    pin::Pin,
10    str::FromStr,
11    sync::Arc,
12    time::Duration,
13};
14
15#[cfg(feature = "_hidden")]
16use async_trait::async_trait;
17use bytes::Bytes;
18use http::{
19    HeaderMap,
20    header::HeaderValue,
21    uri::{Authority, Scheme},
22};
23use rand::RngExt;
24use s2_api::{v1 as api, v1::stream::s2s::CompressionAlgorithm};
25/// Validation error.
26pub use s2_common::ValidationError;
27/// Access token ID.
28///
29/// **Note:** It must be unique to the account and between 1 and 96 bytes in length, and must
30/// not contain NUL bytes.
31pub use s2_common::access::AccessTokenId;
32/// See [`ListAccessTokensInput::prefix`]. It must not contain NUL bytes.
33pub use s2_common::access::AccessTokenIdPrefix;
34/// See [`ListAccessTokensInput::start_after`]. It must not contain NUL bytes.
35pub use s2_common::access::AccessTokenIdStartAfter;
36/// Basin name.
37///
38/// **Note:** It must be globally unique and between 8 and 48 bytes in length. It can only
39/// comprise lowercase letters, numbers, and hyphens. It cannot begin or end with a hyphen.
40pub use s2_common::basin::BasinName;
41/// See [`ListBasinsInput::prefix`].
42pub use s2_common::basin::BasinNamePrefix;
43/// See [`ListBasinsInput::start_after`].
44pub use s2_common::basin::BasinNameStartAfter;
45/// Location name.
46///
47/// **Note:** It must be between 1 and 64 characters in length and can only comprise ASCII
48/// letters, numbers, colons, hyphens, and periods.
49pub use s2_common::location::LocationName;
50/// Stream name.
51///
52/// **Note:** It must be unique to the basin and between 1 and 512 bytes in length, and must
53/// not contain NUL bytes.
54pub use s2_common::stream::StreamName;
55/// See [`ListStreamsInput::prefix`]. It must not contain NUL bytes.
56pub use s2_common::stream::StreamNamePrefix;
57/// See [`ListStreamsInput::start_after`]. It must not contain NUL bytes.
58pub use s2_common::stream::StreamNameStartAfter;
59pub use s2_common::{
60    caps::RECORD_BATCH_MAX,
61    encryption::{EncryptionAlgorithm, EncryptionKey},
62};
63
64pub(crate) const ONE_MIB: u32 = 1024 * 1024;
65
66use s2_common::{
67    maybe::Maybe,
68    record::{MAX_FENCING_TOKEN_LENGTH, Metered, MeteredSize},
69    resources::ProvisionResult,
70};
71use secrecy::SecretString;
72
73use crate::error::RequestError;
74
75#[cfg(feature = "_hidden")]
76#[derive(Debug, Clone, thiserror::Error)]
77#[error("{message}")]
78#[doc(hidden)]
79pub struct AccessTokenProviderError {
80    message: String,
81    retryable: bool,
82}
83
84#[cfg(feature = "_hidden")]
85impl AccessTokenProviderError {
86    /// Create a provider error that should be retried with normal SDK backoff.
87    pub fn transient(message: impl Into<String>) -> Self {
88        Self {
89            message: message.into(),
90            retryable: true,
91        }
92    }
93
94    /// Create a provider error that should be returned immediately.
95    pub fn permanent(message: impl Into<String>) -> Self {
96        Self {
97            message: message.into(),
98            retryable: false,
99        }
100    }
101
102    pub(crate) fn is_retryable(&self) -> bool {
103        self.retryable
104    }
105}
106
107#[cfg(feature = "_hidden")]
108#[async_trait]
109#[doc(hidden)]
110pub trait AccessTokenProvider: fmt::Debug + Send + Sync {
111    /// Return an access token for the next request attempt.
112    async fn access_token(&self) -> Result<String, AccessTokenProviderError>;
113
114    /// Notify the provider that S2 rejected an access token.
115    fn invalidate_access_token(&self, _rejected_access_token: &str) {}
116}
117
118#[derive(Clone)]
119pub(crate) enum AccessToken {
120    Static(SecretString),
121    #[cfg(feature = "_hidden")]
122    Provider(Arc<dyn AccessTokenProvider>),
123}
124
125#[derive(Debug, Clone, Copy, PartialEq, Eq)]
126pub(crate) enum AccessTokenMode {
127    Static,
128    #[cfg(feature = "_hidden")]
129    Refreshable,
130}
131
132impl AccessTokenMode {
133    pub(crate) fn is_refreshable(self) -> bool {
134        match self {
135            Self::Static => false,
136            #[cfg(feature = "_hidden")]
137            Self::Refreshable => true,
138        }
139    }
140}
141
142impl AccessToken {
143    pub(crate) fn mode(&self) -> AccessTokenMode {
144        match self {
145            Self::Static(_) => AccessTokenMode::Static,
146            #[cfg(feature = "_hidden")]
147            Self::Provider(_) => AccessTokenMode::Refreshable,
148        }
149    }
150}
151
152impl fmt::Debug for AccessToken {
153    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
154        match self {
155            Self::Static(_) => formatter.write_str("Static(<redacted>)"),
156            #[cfg(feature = "_hidden")]
157            Self::Provider(_) => formatter.write_str("Provider(<redacted>)"),
158        }
159    }
160}
161
162/// An RFC 3339 datetime.
163///
164/// It can be created in either of the following ways:
165/// - Parse an RFC 3339 datetime string using [`FromStr`] or [`str::parse`].
166/// - Convert from [`time::OffsetDateTime`] using [`TryFrom`]/[`TryInto`].
167#[derive(Debug, Clone, Copy, PartialEq, Eq)]
168pub struct S2DateTime(time::OffsetDateTime);
169
170impl TryFrom<time::OffsetDateTime> for S2DateTime {
171    type Error = ValidationError;
172
173    fn try_from(dt: time::OffsetDateTime) -> Result<Self, Self::Error> {
174        dt.format(&time::format_description::well_known::Rfc3339)
175            .map_err(|e| ValidationError(format!("not a valid RFC 3339 datetime: {e}")))?;
176        Ok(Self(dt))
177    }
178}
179
180impl From<S2DateTime> for time::OffsetDateTime {
181    fn from(dt: S2DateTime) -> Self {
182        dt.0
183    }
184}
185
186impl FromStr for S2DateTime {
187    type Err = ValidationError;
188
189    fn from_str(s: &str) -> Result<Self, Self::Err> {
190        time::OffsetDateTime::parse(s, &time::format_description::well_known::Rfc3339)
191            .map(Self)
192            .map_err(|e| ValidationError(format!("not a valid RFC 3339 datetime: {e}")))
193    }
194}
195
196impl fmt::Display for S2DateTime {
197    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
198        write!(
199            f,
200            "{}",
201            self.0
202                .format(&time::format_description::well_known::Rfc3339)
203                .expect("RFC3339 formatting should not fail for S2DateTime")
204        )
205    }
206}
207
208/// Authority for connecting to an S2 basin.
209#[derive(Debug, Clone, PartialEq)]
210pub(crate) enum BasinAuthority {
211    /// Parent zone for basins. DNS is used to route to the correct cell for the basin.
212    ParentZone(Authority),
213    /// Direct cell authority. Basin is expected to be hosted by this cell.
214    Direct(Authority),
215}
216
217/// Account endpoint.
218#[derive(Debug, Clone)]
219pub struct AccountEndpoint {
220    scheme: Scheme,
221    authority: Authority,
222}
223
224impl AccountEndpoint {
225    /// Create a new [`AccountEndpoint`] with the given endpoint.
226    pub fn new(endpoint: &str) -> Result<Self, ValidationError> {
227        endpoint.parse()
228    }
229}
230
231impl FromStr for AccountEndpoint {
232    type Err = ValidationError;
233
234    fn from_str(s: &str) -> Result<Self, Self::Err> {
235        let (scheme, authority) = match s.find("://") {
236            Some(idx) => {
237                let scheme: Scheme = s[..idx]
238                    .parse()
239                    .map_err(|_| "invalid account endpoint scheme".to_string())?;
240                (scheme, &s[idx + 3..])
241            }
242            None => (Scheme::HTTPS, s),
243        };
244        Ok(Self {
245            scheme,
246            authority: authority
247                .parse()
248                .map_err(|e| format!("invalid account endpoint authority: {e}"))?,
249        })
250    }
251}
252
253/// Basin endpoint.
254#[derive(Debug, Clone)]
255pub struct BasinEndpoint {
256    scheme: Scheme,
257    authority: BasinAuthority,
258}
259
260impl BasinEndpoint {
261    /// Create a new [`BasinEndpoint`] with the given endpoint.
262    pub fn new(endpoint: &str) -> Result<Self, ValidationError> {
263        endpoint.parse()
264    }
265}
266
267impl FromStr for BasinEndpoint {
268    type Err = ValidationError;
269
270    fn from_str(s: &str) -> Result<Self, Self::Err> {
271        let (scheme, authority) = match s.find("://") {
272            Some(idx) => {
273                let scheme: Scheme = s[..idx]
274                    .parse()
275                    .map_err(|_| "invalid basin endpoint scheme".to_string())?;
276                (scheme, &s[idx + 3..])
277            }
278            None => (Scheme::HTTPS, s),
279        };
280        let authority = if let Some(authority) = authority.strip_prefix("{basin}.") {
281            BasinAuthority::ParentZone(
282                authority
283                    .parse()
284                    .map_err(|e| format!("invalid basin endpoint authority: {e}"))?,
285            )
286        } else {
287            BasinAuthority::Direct(
288                authority
289                    .parse()
290                    .map_err(|e| format!("invalid basin endpoint authority: {e}"))?,
291            )
292        };
293        Ok(Self { scheme, authority })
294    }
295}
296
297#[derive(Debug, Clone)]
298#[non_exhaustive]
299/// Endpoints for the S2 environment.
300pub struct S2Endpoints {
301    pub(crate) scheme: Scheme,
302    pub(crate) account_authority: Authority,
303    pub(crate) basin_authority: BasinAuthority,
304}
305
306impl S2Endpoints {
307    /// Create a new [`S2Endpoints`] with the given account and basin endpoints.
308    pub fn new(
309        account_endpoint: AccountEndpoint,
310        basin_endpoint: BasinEndpoint,
311    ) -> Result<Self, ValidationError> {
312        if account_endpoint.scheme != basin_endpoint.scheme {
313            return Err("account and basin endpoints must have the same scheme".into());
314        }
315        Ok(Self {
316            scheme: account_endpoint.scheme,
317            account_authority: account_endpoint.authority,
318            basin_authority: basin_endpoint.authority,
319        })
320    }
321
322    /// Create endpoints for a single account and basin endpoint.
323    ///
324    /// This is useful for S2-compatible services that expose both APIs at one endpoint.
325    pub fn for_endpoint(endpoint: &str) -> Result<Self, ValidationError> {
326        Self::new(
327            AccountEndpoint::new(endpoint)?,
328            BasinEndpoint::new(endpoint)?,
329        )
330    }
331
332    /// Create a new [`S2Endpoints`] from environment variables.
333    ///
334    /// The following environment variables are expected to be set:
335    /// - `S2_ACCOUNT_ENDPOINT` - Account-level endpoint.
336    /// - `S2_BASIN_ENDPOINT` - Basin-level endpoint.
337    pub fn from_env() -> Result<Self, ValidationError> {
338        let account_endpoint: AccountEndpoint = match std::env::var("S2_ACCOUNT_ENDPOINT") {
339            Ok(endpoint) => endpoint.parse()?,
340            Err(VarError::NotPresent) => return Err("S2_ACCOUNT_ENDPOINT env var not set".into()),
341            Err(VarError::NotUnicode(_)) => {
342                return Err("S2_ACCOUNT_ENDPOINT is not valid unicode".into());
343            }
344        };
345
346        let basin_endpoint: BasinEndpoint = match std::env::var("S2_BASIN_ENDPOINT") {
347            Ok(endpoint) => endpoint.parse()?,
348            Err(VarError::NotPresent) => return Err("S2_BASIN_ENDPOINT env var not set".into()),
349            Err(VarError::NotUnicode(_)) => {
350                return Err("S2_BASIN_ENDPOINT is not valid unicode".into());
351            }
352        };
353
354        if account_endpoint.scheme != basin_endpoint.scheme {
355            return Err(
356                "S2_ACCOUNT_ENDPOINT and S2_BASIN_ENDPOINT must have the same scheme".into(),
357            );
358        }
359
360        Ok(Self {
361            scheme: account_endpoint.scheme,
362            account_authority: account_endpoint.authority,
363            basin_authority: basin_endpoint.authority,
364        })
365    }
366
367    /// Return the default S2 Cloud endpoints.
368    pub fn for_cloud() -> Self {
369        Self {
370            scheme: Scheme::HTTPS,
371            account_authority: "a.s2.dev".try_into().expect("valid authority"),
372            basin_authority: BasinAuthority::ParentZone(
373                "b.s2.dev".try_into().expect("valid authority"),
374            ),
375        }
376    }
377}
378
379#[derive(Debug, Clone, Copy)]
380/// Compression algorithm for request and response bodies.
381pub enum Compression {
382    /// No compression.
383    None,
384    /// Gzip compression.
385    Gzip,
386    /// Zstd compression.
387    Zstd,
388}
389
390impl From<Compression> for CompressionAlgorithm {
391    fn from(value: Compression) -> Self {
392        match value {
393            Compression::None => CompressionAlgorithm::None,
394            Compression::Gzip => CompressionAlgorithm::Gzip,
395            Compression::Zstd => CompressionAlgorithm::Zstd,
396        }
397    }
398}
399
400#[derive(Debug, Clone, Copy, PartialEq)]
401#[non_exhaustive]
402/// Retry policy for [`append`](crate::S2Stream::append) and
403/// [`append_session`](crate::S2Stream::append_session) operations.
404pub enum AppendRetryPolicy {
405    /// Retry all appends. Use when duplicate records on the stream are acceptable.
406    All,
407    /// Retry when it can be determined that the request had no side effects.
408    ///
409    /// Uses a frame-level signal to detect whether any body frames were consumed
410    /// by the HTTP transport. If no frames were sent, the server never saw the
411    /// request, so retry is safe and will not cause duplicate records.
412    ///
413    /// Certain server errors (`rate_limited`, `hot_server`) are also safe to
414    /// retry regardless of frame signal state, since they guarantee no mutation
415    /// occurred.
416    NoSideEffects,
417}
418
419#[derive(Debug, Clone)]
420#[non_exhaustive]
421/// Configuration for retrying requests in case of transient failures.
422///
423/// Exponential backoff with jitter is the retry strategy. Below is the pseudocode for the strategy:
424/// ```text
425/// base_delay = min(min_base_delay · 2ⁿ, max_base_delay)    (n = retry attempt, starting from 0)
426///     jitter = rand[0, base_delay]
427///     delay  = base_delay + jitter
428/// ````
429pub struct RetryConfig {
430    /// Total number of attempts including the initial try. A value of `1` means no retries.
431    ///
432    /// Defaults to `3`.
433    pub max_attempts: NonZeroU32,
434    /// Minimum base delay for retries.
435    ///
436    /// Defaults to `100ms`.
437    pub min_base_delay: Duration,
438    /// Maximum base delay for retries.
439    ///
440    /// Defaults to `1s`.
441    pub max_base_delay: Duration,
442    /// Retry policy for [`append`](crate::S2Stream::append) and
443    /// [`append_session`](crate::S2Stream::append_session) operations.
444    ///
445    /// Defaults to `All`.
446    pub append_retry_policy: AppendRetryPolicy,
447}
448
449impl Default for RetryConfig {
450    fn default() -> Self {
451        Self {
452            max_attempts: NonZeroU32::new(3).expect("valid non-zero u32"),
453            min_base_delay: Duration::from_millis(100),
454            max_base_delay: Duration::from_secs(1),
455            append_retry_policy: AppendRetryPolicy::All,
456        }
457    }
458}
459
460impl RetryConfig {
461    /// Create a new [`RetryConfig`] with default settings.
462    pub fn new() -> Self {
463        Self::default()
464    }
465
466    pub(crate) fn max_retries(&self) -> u32 {
467        self.max_attempts.get() - 1
468    }
469
470    /// Set the total number of attempts including the initial try.
471    pub fn with_max_attempts(self, max_attempts: NonZeroU32) -> Self {
472        Self {
473            max_attempts,
474            ..self
475        }
476    }
477
478    /// Set the minimum base delay for retries.
479    pub fn with_min_base_delay(self, min_base_delay: Duration) -> Self {
480        Self {
481            min_base_delay,
482            ..self
483        }
484    }
485
486    /// Set the maximum base delay for retries.
487    pub fn with_max_base_delay(self, max_base_delay: Duration) -> Self {
488        Self {
489            max_base_delay,
490            ..self
491        }
492    }
493
494    /// Set the retry policy for [`append`](crate::S2Stream::append) and
495    /// [`append_session`](crate::S2Stream::append_session) operations.
496    pub fn with_append_retry_policy(self, append_retry_policy: AppendRetryPolicy) -> Self {
497        Self {
498            append_retry_policy,
499            ..self
500        }
501    }
502}
503
504#[derive(Debug, Clone)]
505#[non_exhaustive]
506/// Configuration for [`S2`](crate::S2).
507pub struct S2Config {
508    pub(crate) access_token: AccessToken,
509    pub(crate) endpoints: S2Endpoints,
510    pub(crate) connection_timeout: Duration,
511    pub(crate) request_timeout: Duration,
512    pub(crate) retry: RetryConfig,
513    pub(crate) compression: Compression,
514    pub(crate) user_agent: HeaderValue,
515    pub(crate) default_headers: HeaderMap,
516    pub(crate) insecure_skip_cert_verification: bool,
517    pub(crate) rustls_crypto_provider: Option<Arc<rustls::crypto::CryptoProvider>>,
518}
519
520impl S2Config {
521    /// Create a new [`S2Config`] with the given access token and default settings.
522    pub fn new(access_token: impl Into<String>) -> Self {
523        Self {
524            access_token: AccessToken::Static(access_token.into().into()),
525            endpoints: S2Endpoints::for_cloud(),
526            connection_timeout: Duration::from_secs(3),
527            request_timeout: Duration::from_secs(5),
528            retry: RetryConfig::new(),
529            compression: Compression::None,
530            user_agent: concat!("s2-sdk-rust/", env!("CARGO_PKG_VERSION"))
531                .parse()
532                .expect("valid user agent"),
533            default_headers: HeaderMap::new(),
534            insecure_skip_cert_verification: false,
535            rustls_crypto_provider: default_rustls_crypto_provider(),
536        }
537    }
538
539    #[cfg(feature = "_hidden")]
540    #[doc(hidden)]
541    pub fn with_access_token_provider(self, provider: impl AccessTokenProvider + 'static) -> Self {
542        Self {
543            access_token: AccessToken::Provider(Arc::new(provider)),
544            ..self
545        }
546    }
547
548    /// Set the S2 endpoints to connect to.
549    pub fn with_endpoints(self, endpoints: S2Endpoints) -> Self {
550        Self { endpoints, ..self }
551    }
552
553    /// Set additional HTTP headers to send with every request.
554    ///
555    /// These headers apply to account, basin, and stream operations, including
556    /// retries and streaming requests. SDK-generated headers, such as
557    /// authorization and basin routing, take precedence over these defaults.
558    /// Calling this method again replaces the previous set of default headers.
559    ///
560    /// `Accept-Encoding` defaults are used only when [`Compression::None`] is
561    /// configured; otherwise the SDK sets the header to the configured
562    /// compression algorithm.
563    ///
564    /// Headers are sent to all configured S2 endpoints. Use
565    /// [`HeaderValue::set_sensitive`] for values that should be redacted in debug
566    /// output. Do not use these defaults for per-request identifiers, since the
567    /// same values are reused across requests.
568    ///
569    /// # Errors
570    ///
571    /// Returns an error if `default_headers` contains `Content-Type`,
572    /// `Content-Encoding`, `Content-Length`, or `Transfer-Encoding`.
573    /// The SDK controls request format and body framing.
574    /// Use [`Self::with_compression`] to configure request body encoding.
575    #[cfg(feature = "_hidden")]
576    #[doc(hidden)]
577    pub fn with_default_headers(self, default_headers: HeaderMap) -> Result<Self, ValidationError> {
578        if default_headers.contains_key(http::header::CONTENT_ENCODING) {
579            return Err(ValidationError(
580                "Content-Encoding cannot be set in default headers; use S2Config::with_compression instead"
581                    .into(),
582            ));
583        }
584        for name in [
585            http::header::CONTENT_TYPE,
586            http::header::CONTENT_LENGTH,
587            http::header::TRANSFER_ENCODING,
588        ] {
589            if default_headers.contains_key(&name) {
590                return Err(ValidationError(format!(
591                    "{name} cannot be set in default headers; the SDK controls request format and body framing"
592                )));
593            }
594        }
595        Ok(Self {
596            default_headers,
597            ..self
598        })
599    }
600
601    /// Set the timeout for establishing a connection to the server.
602    ///
603    /// Defaults to `3s`.
604    pub fn with_connection_timeout(self, connection_timeout: Duration) -> Self {
605        Self {
606            connection_timeout,
607            ..self
608        }
609    }
610
611    /// Set the timeout for requests.
612    ///
613    /// Defaults to `5s`.
614    pub fn with_request_timeout(self, request_timeout: Duration) -> Self {
615        Self {
616            request_timeout,
617            ..self
618        }
619    }
620
621    /// Set the retry configuration for requests.
622    ///
623    /// See [`RetryConfig`] for defaults.
624    pub fn with_retry(self, retry: RetryConfig) -> Self {
625        Self { retry, ..self }
626    }
627
628    /// Set the compression algorithm for requests and responses.
629    ///
630    /// Defaults to no compression.
631    pub fn with_compression(self, compression: Compression) -> Self {
632        Self {
633            compression,
634            ..self
635        }
636    }
637
638    /// Skip TLS certificate verification (insecure).
639    ///
640    /// This is useful for connecting to endpoints with self-signed certificates
641    /// or certificates that don't match the hostname (similar to `curl -k`).
642    ///
643    /// # Warning
644    ///
645    /// This disables certificate verification and should only be used for
646    /// testing or development purposes. **Never use this in production.**
647    ///
648    /// Defaults to `false`.
649    pub fn with_insecure_skip_cert_verification(self, skip: bool) -> Self {
650        Self {
651            insecure_skip_cert_verification: skip,
652            ..self
653        }
654    }
655
656    /// Use a specific rustls crypto provider for SDK TLS connections.
657    ///
658    /// With default features enabled, the SDK uses the `aws-lc-rs` provider.
659    /// With default features disabled, the SDK uses rustls's process-global
660    /// provider if one has been installed, or returns an error otherwise.
661    ///
662    /// Use this when your application needs a specific rustls provider, such as
663    /// `ring` or a custom [`rustls::crypto::CryptoProvider`]. The corresponding
664    /// rustls provider feature must be enabled in the dependency graph.
665    pub fn with_rustls_crypto_provider(
666        self,
667        provider: impl Into<Arc<rustls::crypto::CryptoProvider>>,
668    ) -> Self {
669        Self {
670            rustls_crypto_provider: Some(provider.into()),
671            ..self
672        }
673    }
674
675    /// Use rustls's `aws-lc-rs` crypto provider.
676    ///
677    /// Requires the `rustls-aws-lc-rs` crate feature.
678    #[cfg(feature = "rustls-aws-lc-rs")]
679    pub fn with_rustls_aws_lc_rs_crypto_provider(self) -> Self {
680        self.with_rustls_crypto_provider(rustls::crypto::aws_lc_rs::default_provider())
681    }
682
683    /// Use rustls's `ring` crypto provider.
684    ///
685    /// Requires the `rustls-ring` crate feature.
686    #[cfg(feature = "rustls-ring")]
687    pub fn with_rustls_ring_crypto_provider(self) -> Self {
688        self.with_rustls_crypto_provider(rustls::crypto::ring::default_provider())
689    }
690
691    #[doc(hidden)]
692    #[cfg(feature = "_hidden")]
693    pub fn with_user_agent(self, user_agent: impl Into<String>) -> Result<Self, ValidationError> {
694        let user_agent = user_agent
695            .into()
696            .parse()
697            .map_err(|e| ValidationError(format!("invalid user agent: {e}")))?;
698        Ok(Self { user_agent, ..self })
699    }
700}
701
702#[cfg(feature = "rustls-aws-lc-rs")]
703fn default_rustls_crypto_provider() -> Option<Arc<rustls::crypto::CryptoProvider>> {
704    Some(Arc::new(rustls::crypto::aws_lc_rs::default_provider()))
705}
706
707#[cfg(all(not(feature = "rustls-aws-lc-rs"), feature = "rustls-ring"))]
708fn default_rustls_crypto_provider() -> Option<Arc<rustls::crypto::CryptoProvider>> {
709    Some(Arc::new(rustls::crypto::ring::default_provider()))
710}
711
712#[cfg(all(not(feature = "rustls-aws-lc-rs"), not(feature = "rustls-ring")))]
713fn default_rustls_crypto_provider() -> Option<Arc<rustls::crypto::CryptoProvider>> {
714    None
715}
716
717#[derive(Debug, Default, Clone, PartialEq, Eq)]
718#[non_exhaustive]
719/// A page of values.
720pub struct Page<T> {
721    /// Values in this page.
722    pub values: Vec<T>,
723    /// Whether there are more pages.
724    pub has_more: bool,
725}
726
727impl<T> Page<T> {
728    pub(crate) fn new(values: impl Into<Vec<T>>, has_more: bool) -> Self {
729        Self {
730            values: values.into(),
731            has_more,
732        }
733    }
734}
735
736#[derive(Debug, Clone, Copy, PartialEq, Eq)]
737/// Storage class for recent appends.
738pub enum StorageClass {
739    /// Standard storage class that offers append latencies under `500ms`.
740    Standard,
741    /// Express storage class that offers append latencies under `50ms`.
742    Express,
743}
744
745impl From<api::config::StorageClass> for StorageClass {
746    fn from(value: api::config::StorageClass) -> Self {
747        match value {
748            api::config::StorageClass::Standard => StorageClass::Standard,
749            api::config::StorageClass::Express => StorageClass::Express,
750        }
751    }
752}
753
754impl From<StorageClass> for api::config::StorageClass {
755    fn from(value: StorageClass) -> Self {
756        match value {
757            StorageClass::Standard => api::config::StorageClass::Standard,
758            StorageClass::Express => api::config::StorageClass::Express,
759        }
760    }
761}
762
763#[derive(Debug, Clone, Copy, PartialEq, Eq)]
764/// Retention policy for records in a stream.
765pub enum RetentionPolicy {
766    /// Age in seconds. Records older than this age are automatically trimmed.
767    Age(u64),
768    /// Records are retained indefinitely unless explicitly trimmed.
769    Infinite,
770}
771
772impl From<api::config::RetentionPolicy> for RetentionPolicy {
773    fn from(value: api::config::RetentionPolicy) -> Self {
774        match value {
775            api::config::RetentionPolicy::Age(secs) => RetentionPolicy::Age(secs),
776            api::config::RetentionPolicy::Infinite(_) => RetentionPolicy::Infinite,
777        }
778    }
779}
780
781impl From<RetentionPolicy> for api::config::RetentionPolicy {
782    fn from(value: RetentionPolicy) -> Self {
783        match value {
784            RetentionPolicy::Age(secs) => api::config::RetentionPolicy::Age(secs),
785            RetentionPolicy::Infinite => {
786                api::config::RetentionPolicy::Infinite(api::config::InfiniteRetention {})
787            }
788        }
789    }
790}
791
792#[derive(Debug, Clone, Copy, PartialEq, Eq)]
793/// Timestamping mode for appends that influences how timestamps are handled.
794pub enum TimestampingMode {
795    /// Prefer client-specified timestamp if present otherwise use arrival time.
796    ClientPrefer,
797    /// Require a client-specified timestamp and reject the append if it is missing.
798    ClientRequire,
799    /// Use the arrival time and ignore any client-specified timestamp.
800    Arrival,
801}
802
803impl From<api::config::TimestampingMode> for TimestampingMode {
804    fn from(value: api::config::TimestampingMode) -> Self {
805        match value {
806            api::config::TimestampingMode::ClientPrefer => TimestampingMode::ClientPrefer,
807            api::config::TimestampingMode::ClientRequire => TimestampingMode::ClientRequire,
808            api::config::TimestampingMode::Arrival => TimestampingMode::Arrival,
809        }
810    }
811}
812
813impl From<TimestampingMode> for api::config::TimestampingMode {
814    fn from(value: TimestampingMode) -> Self {
815        match value {
816            TimestampingMode::ClientPrefer => api::config::TimestampingMode::ClientPrefer,
817            TimestampingMode::ClientRequire => api::config::TimestampingMode::ClientRequire,
818            TimestampingMode::Arrival => api::config::TimestampingMode::Arrival,
819        }
820    }
821}
822
823#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
824#[non_exhaustive]
825/// Configuration for timestamping behavior.
826pub struct TimestampingConfig {
827    /// Timestamping mode for appends that influences how timestamps are handled.
828    ///
829    /// Defaults to [`ClientPrefer`](TimestampingMode::ClientPrefer).
830    pub mode: Option<TimestampingMode>,
831    /// Whether client-specified timestamps are allowed to exceed the arrival time.
832    ///
833    /// Defaults to `false` (client timestamps are capped at the arrival time).
834    pub uncapped: Option<bool>,
835}
836
837impl TimestampingConfig {
838    /// Create a new [`TimestampingConfig`] with default settings.
839    pub fn new() -> Self {
840        Self::default()
841    }
842
843    /// Set the timestamping mode for appends that influences how timestamps are handled.
844    pub fn with_mode(self, mode: TimestampingMode) -> Self {
845        Self {
846            mode: Some(mode),
847            ..self
848        }
849    }
850
851    /// Set whether client-specified timestamps are allowed to exceed the arrival time.
852    pub fn with_uncapped(self, uncapped: bool) -> Self {
853        Self {
854            uncapped: Some(uncapped),
855            ..self
856        }
857    }
858}
859
860impl From<api::config::TimestampingConfig> for TimestampingConfig {
861    fn from(value: api::config::TimestampingConfig) -> Self {
862        Self {
863            mode: value.mode.map(Into::into),
864            uncapped: value.uncapped,
865        }
866    }
867}
868
869impl From<TimestampingConfig> for api::config::TimestampingConfig {
870    fn from(value: TimestampingConfig) -> Self {
871        Self {
872            mode: value.mode.map(Into::into),
873            uncapped: value.uncapped,
874        }
875    }
876}
877
878#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
879#[non_exhaustive]
880/// Configuration for automatically deleting a stream when it becomes empty.
881pub struct DeleteOnEmptyConfig {
882    /// Minimum age in seconds before an empty stream can be deleted.
883    ///
884    /// Defaults to `0` (disables automatic deletion).
885    pub min_age_secs: u64,
886}
887
888impl DeleteOnEmptyConfig {
889    /// Create a new [`DeleteOnEmptyConfig`] with default settings.
890    pub fn new() -> Self {
891        Self::default()
892    }
893
894    /// Set the minimum age in seconds before an empty stream can be deleted.
895    pub fn with_min_age(self, min_age: Duration) -> Self {
896        Self {
897            min_age_secs: min_age.as_secs(),
898        }
899    }
900}
901
902impl From<api::config::DeleteOnEmptyConfig> for DeleteOnEmptyConfig {
903    fn from(value: api::config::DeleteOnEmptyConfig) -> Self {
904        Self {
905            min_age_secs: value.min_age_secs,
906        }
907    }
908}
909
910impl From<DeleteOnEmptyConfig> for api::config::DeleteOnEmptyConfig {
911    fn from(value: DeleteOnEmptyConfig) -> Self {
912        Self {
913            min_age_secs: value.min_age_secs,
914        }
915    }
916}
917
918#[derive(Debug, Clone, Default, PartialEq, Eq)]
919#[non_exhaustive]
920/// Configuration for a stream.
921pub struct StreamConfig {
922    /// Storage class for the stream.
923    ///
924    /// Defaults to [`Express`](StorageClass::Express).
925    pub storage_class: Option<StorageClass>,
926    /// Retention policy for records in the stream.
927    ///
928    /// Defaults to `7 days` of retention.
929    pub retention_policy: Option<RetentionPolicy>,
930    /// Configuration for timestamping behavior.
931    ///
932    /// See [`TimestampingConfig`] for defaults.
933    pub timestamping: Option<TimestampingConfig>,
934    /// Configuration for automatically deleting the stream when it becomes empty.
935    ///
936    /// See [`DeleteOnEmptyConfig`] for defaults.
937    pub delete_on_empty: Option<DeleteOnEmptyConfig>,
938}
939
940impl StreamConfig {
941    /// Create a new [`StreamConfig`] with default settings.
942    pub fn new() -> Self {
943        Self::default()
944    }
945
946    /// Set the storage class for the stream.
947    pub fn with_storage_class(self, storage_class: StorageClass) -> Self {
948        Self {
949            storage_class: Some(storage_class),
950            ..self
951        }
952    }
953
954    /// Set the retention policy for records in the stream.
955    pub fn with_retention_policy(self, retention_policy: RetentionPolicy) -> Self {
956        Self {
957            retention_policy: Some(retention_policy),
958            ..self
959        }
960    }
961
962    /// Set the configuration for timestamping behavior.
963    pub fn with_timestamping(self, timestamping: TimestampingConfig) -> Self {
964        Self {
965            timestamping: Some(timestamping),
966            ..self
967        }
968    }
969
970    /// Set the configuration for automatically deleting the stream when it becomes empty.
971    pub fn with_delete_on_empty(self, delete_on_empty: DeleteOnEmptyConfig) -> Self {
972        Self {
973            delete_on_empty: Some(delete_on_empty),
974            ..self
975        }
976    }
977}
978
979impl From<api::config::StreamConfig> for StreamConfig {
980    fn from(value: api::config::StreamConfig) -> Self {
981        Self {
982            storage_class: value.storage_class.map(Into::into),
983            retention_policy: value.retention_policy.map(Into::into),
984            timestamping: value.timestamping.map(Into::into),
985            delete_on_empty: value.delete_on_empty.map(Into::into),
986        }
987    }
988}
989
990impl From<StreamConfig> for api::config::StreamConfig {
991    fn from(value: StreamConfig) -> Self {
992        Self {
993            storage_class: value.storage_class.map(Into::into),
994            retention_policy: value.retention_policy.map(Into::into),
995            timestamping: value.timestamping.map(Into::into),
996            delete_on_empty: value.delete_on_empty.map(Into::into),
997        }
998    }
999}
1000
1001#[derive(Debug, Clone, Default, PartialEq, Eq)]
1002#[non_exhaustive]
1003/// Configuration for a basin.
1004pub struct BasinConfig {
1005    /// Default configuration for all streams in the basin.
1006    ///
1007    /// See [`StreamConfig`] for defaults.
1008    pub default_stream_config: Option<StreamConfig>,
1009    /// Encryption algorithm to apply to newly created streams in the basin.
1010    pub stream_cipher: Option<EncryptionAlgorithm>,
1011    /// Whether to create stream on append if it doesn't exist using default stream configuration.
1012    ///
1013    /// Defaults to `false`.
1014    pub create_stream_on_append: bool,
1015    /// Whether to create stream on read if it doesn't exist using default stream configuration.
1016    ///
1017    /// Defaults to `false`.
1018    pub create_stream_on_read: bool,
1019}
1020
1021impl BasinConfig {
1022    /// Create a new [`BasinConfig`] with default settings.
1023    pub fn new() -> Self {
1024        Self::default()
1025    }
1026
1027    /// Set the default configuration for all streams in the basin.
1028    pub fn with_default_stream_config(self, config: StreamConfig) -> Self {
1029        Self {
1030            default_stream_config: Some(config),
1031            ..self
1032        }
1033    }
1034
1035    /// Set the encryption algorithm to apply to newly created streams in the basin.
1036    pub fn with_stream_cipher(self, stream_cipher: EncryptionAlgorithm) -> Self {
1037        Self {
1038            stream_cipher: Some(stream_cipher),
1039            ..self
1040        }
1041    }
1042
1043    /// Set whether to create stream on append if it doesn't exist using default stream
1044    /// configuration.
1045    pub fn with_create_stream_on_append(self, create_stream_on_append: bool) -> Self {
1046        Self {
1047            create_stream_on_append,
1048            ..self
1049        }
1050    }
1051
1052    /// Set whether to create stream on read if it doesn't exist using default stream configuration.
1053    pub fn with_create_stream_on_read(self, create_stream_on_read: bool) -> Self {
1054        Self {
1055            create_stream_on_read,
1056            ..self
1057        }
1058    }
1059}
1060
1061impl From<api::config::BasinConfig> for BasinConfig {
1062    fn from(value: api::config::BasinConfig) -> Self {
1063        Self {
1064            default_stream_config: value.default_stream_config.map(Into::into),
1065            stream_cipher: value.stream_cipher.map(Into::into),
1066            create_stream_on_append: value.create_stream_on_append,
1067            create_stream_on_read: value.create_stream_on_read,
1068        }
1069    }
1070}
1071
1072impl From<BasinConfig> for api::config::BasinConfig {
1073    fn from(value: BasinConfig) -> Self {
1074        Self {
1075            default_stream_config: value.default_stream_config.map(Into::into),
1076            stream_cipher: value.stream_cipher.map(Into::into),
1077            create_stream_on_append: value.create_stream_on_append,
1078            create_stream_on_read: value.create_stream_on_read,
1079        }
1080    }
1081}
1082
1083#[derive(Debug, Clone)]
1084#[non_exhaustive]
1085/// Input for [`create_basin`](crate::S2::create_basin) operation.
1086pub struct CreateBasinInput {
1087    /// Basin name.
1088    pub name: BasinName,
1089    /// Configuration for the basin.
1090    ///
1091    /// See [`BasinConfig`] for defaults.
1092    pub config: Option<BasinConfig>,
1093    /// Location of the basin.
1094    ///
1095    /// If omitted when creating, uses the default location for the account.
1096    pub location: Option<LocationName>,
1097    idempotency_token: String,
1098}
1099
1100impl CreateBasinInput {
1101    /// Create a new [`CreateBasinInput`] with the given basin name.
1102    pub fn new(name: BasinName) -> Self {
1103        Self {
1104            name,
1105            config: None,
1106            location: None,
1107            idempotency_token: idempotency_token(),
1108        }
1109    }
1110
1111    /// Set the configuration for the basin.
1112    pub fn with_config(self, config: BasinConfig) -> Self {
1113        Self {
1114            config: Some(config),
1115            ..self
1116        }
1117    }
1118
1119    /// Set the location of the basin.
1120    pub fn with_location<S>(self, location: S) -> Result<Self, ValidationError>
1121    where
1122        S: TryInto<LocationName>,
1123        S::Error: fmt::Display,
1124    {
1125        let location = location
1126            .try_into()
1127            .map_err(|e| ValidationError(e.to_string()))?;
1128        Ok(Self {
1129            location: Some(location),
1130            ..self
1131        })
1132    }
1133}
1134
1135impl From<CreateBasinInput> for (api::basin::CreateBasinRequest, String) {
1136    fn from(value: CreateBasinInput) -> Self {
1137        (
1138            api::basin::CreateBasinRequest {
1139                basin: value.name,
1140                config: value.config.map(Into::into),
1141                location: value.location,
1142            },
1143            value.idempotency_token,
1144        )
1145    }
1146}
1147
1148#[derive(Debug, Clone)]
1149#[non_exhaustive]
1150/// Input for [`ensure_basin`](crate::S2::ensure_basin) operation.
1151pub struct EnsureBasinInput {
1152    /// Basin name.
1153    pub name: BasinName,
1154    /// Configuration for the basin.
1155    ///
1156    /// See [`BasinConfig`] for defaults.
1157    pub config: Option<BasinConfig>,
1158    /// Location of the basin.
1159    ///
1160    /// If omitted when creating, uses the default location for the account. Cannot be changed once
1161    /// set.
1162    pub location: Option<LocationName>,
1163}
1164
1165impl EnsureBasinInput {
1166    /// Create a new [`EnsureBasinInput`] with the given basin name.
1167    pub fn new(name: BasinName) -> Self {
1168        Self {
1169            name,
1170            config: None,
1171            location: None,
1172        }
1173    }
1174
1175    /// Set the configuration for the basin.
1176    pub fn with_config(self, config: BasinConfig) -> Self {
1177        Self {
1178            config: Some(config),
1179            ..self
1180        }
1181    }
1182
1183    /// Set the location of the basin.
1184    pub fn with_location<S>(self, location: S) -> Result<Self, ValidationError>
1185    where
1186        S: TryInto<LocationName>,
1187        S::Error: fmt::Display,
1188    {
1189        let location = location
1190            .try_into()
1191            .map_err(|e| ValidationError(e.to_string()))?;
1192        Ok(Self {
1193            location: Some(location),
1194            ..self
1195        })
1196    }
1197}
1198
1199impl From<EnsureBasinInput> for (BasinName, Option<api::basin::EnsureBasinRequest>) {
1200    fn from(value: EnsureBasinInput) -> Self {
1201        let config = value.config;
1202        let request = if config.is_some() || value.location.is_some() {
1203            Some(api::basin::EnsureBasinRequest {
1204                config: config.map(Into::into),
1205                location: value.location,
1206            })
1207        } else {
1208            None
1209        };
1210        (value.name, request)
1211    }
1212}
1213
1214#[derive(Debug, Clone)]
1215/// Output for `ensure` operations ([`ensure_basin`](crate::S2::ensure_basin),
1216/// [`ensure_stream`](crate::S2Basin::ensure_stream)).
1217pub enum EnsureOutput<T> {
1218    /// Resource created.
1219    Created(T),
1220    /// Resource already existed, and its config was updated.
1221    ConfigUpdated(T),
1222    /// Resource already existed, and its config is unchanged.
1223    ConfigUnchanged(T),
1224}
1225
1226impl<T> From<ProvisionResult<T>> for EnsureOutput<T> {
1227    fn from(result: ProvisionResult<T>) -> Self {
1228        match result {
1229            ProvisionResult::Created(info) => EnsureOutput::Created(info),
1230            ProvisionResult::Updated(info) => EnsureOutput::ConfigUpdated(info),
1231            ProvisionResult::Noop(info) => EnsureOutput::ConfigUnchanged(info),
1232        }
1233    }
1234}
1235
1236#[derive(Debug, Clone, Default)]
1237#[non_exhaustive]
1238/// Input for [`list_basins`](crate::S2::list_basins) operation.
1239pub struct ListBasinsInput {
1240    /// Filter basins whose names begin with this value.
1241    ///
1242    /// Defaults to `""`.
1243    pub prefix: BasinNamePrefix,
1244    /// Filter basins whose names are lexicographically greater than this value.
1245    ///
1246    /// Defaults to `""`.
1247    pub start_after: BasinNameStartAfter,
1248    /// Number of basins to return in a page. Will be clamped to a maximum of `1000`.
1249    ///
1250    /// Defaults to `1000`.
1251    pub limit: Option<usize>,
1252}
1253
1254impl ListBasinsInput {
1255    /// Create a new [`ListBasinsInput`] with default values.
1256    pub fn new() -> Self {
1257        Self::default()
1258    }
1259
1260    /// Set the prefix used to filter basins whose names begin with this value.
1261    pub fn with_prefix(self, prefix: BasinNamePrefix) -> Self {
1262        Self { prefix, ..self }
1263    }
1264
1265    /// Set the value used to filter basins whose names are lexicographically greater than this
1266    /// value.
1267    pub fn with_start_after(self, start_after: BasinNameStartAfter) -> Self {
1268        Self {
1269            start_after,
1270            ..self
1271        }
1272    }
1273
1274    /// Set the limit on number of basins to return in a page.
1275    pub fn with_limit(self, limit: usize) -> Self {
1276        Self {
1277            limit: Some(limit),
1278            ..self
1279        }
1280    }
1281}
1282
1283impl From<ListBasinsInput> for api::basin::ListBasinsRequest {
1284    fn from(value: ListBasinsInput) -> Self {
1285        Self {
1286            prefix: Some(value.prefix),
1287            start_after: Some(value.start_after),
1288            limit: value.limit,
1289        }
1290    }
1291}
1292
1293#[derive(Debug, Clone, Default)]
1294/// Input for [`list_all_basins`](crate::S2::list_all_basins) operation.
1295pub struct ListAllBasinsInput {
1296    /// Filter basins whose names begin with this value.
1297    ///
1298    /// Defaults to `""`.
1299    pub prefix: BasinNamePrefix,
1300    /// Filter basins whose names are lexicographically greater than this value.
1301    ///
1302    /// Defaults to `""`.
1303    pub start_after: BasinNameStartAfter,
1304    /// Whether to include basins that are being deleted.
1305    ///
1306    /// Defaults to `false`.
1307    pub include_deleted: bool,
1308}
1309
1310impl ListAllBasinsInput {
1311    /// Create a new [`ListAllBasinsInput`] with default values.
1312    pub fn new() -> Self {
1313        Self::default()
1314    }
1315
1316    /// Set the prefix used to filter basins whose names begin with this value.
1317    pub fn with_prefix(self, prefix: BasinNamePrefix) -> Self {
1318        Self { prefix, ..self }
1319    }
1320
1321    /// Set the value used to filter basins whose names are lexicographically greater than this
1322    /// value.
1323    pub fn with_start_after(self, start_after: BasinNameStartAfter) -> Self {
1324        Self {
1325            start_after,
1326            ..self
1327        }
1328    }
1329
1330    /// Set whether to include basins that are being deleted.
1331    pub fn with_include_deleted(self, include_deleted: bool) -> Self {
1332        Self {
1333            include_deleted,
1334            ..self
1335        }
1336    }
1337}
1338
1339#[derive(Debug, Clone, PartialEq, Eq)]
1340#[non_exhaustive]
1341/// Basin information.
1342pub struct BasinInfo {
1343    /// Basin name.
1344    pub name: BasinName,
1345    /// Location of the basin.
1346    pub location: Option<LocationName>,
1347    /// Creation time.
1348    pub created_at: S2DateTime,
1349    /// Deletion time if the basin is being deleted.
1350    pub deleted_at: Option<S2DateTime>,
1351}
1352
1353impl TryFrom<api::basin::BasinInfo> for BasinInfo {
1354    type Error = ValidationError;
1355
1356    fn try_from(value: api::basin::BasinInfo) -> Result<Self, Self::Error> {
1357        Ok(Self {
1358            name: value.name,
1359            location: value.location,
1360            created_at: value.created_at.try_into()?,
1361            deleted_at: value.deleted_at.map(S2DateTime::try_from).transpose()?,
1362        })
1363    }
1364}
1365
1366#[derive(Debug, Clone)]
1367#[non_exhaustive]
1368/// Input for [`delete_basin`](crate::S2::delete_basin) operation.
1369pub struct DeleteBasinInput {
1370    /// Basin name.
1371    pub name: BasinName,
1372    /// Whether to ignore `Not Found` error if the basin doesn't exist.
1373    pub ignore_not_found: bool,
1374}
1375
1376impl DeleteBasinInput {
1377    /// Create a new [`DeleteBasinInput`] with the given basin name.
1378    pub fn new(name: BasinName) -> Self {
1379        Self {
1380            name,
1381            ignore_not_found: false,
1382        }
1383    }
1384
1385    /// Set whether to ignore `Not Found` error if the basin is not existing.
1386    pub fn with_ignore_not_found(self, ignore_not_found: bool) -> Self {
1387        Self {
1388            ignore_not_found,
1389            ..self
1390        }
1391    }
1392}
1393
1394#[derive(Debug, Clone, Default)]
1395#[non_exhaustive]
1396/// Reconfiguration for [`TimestampingConfig`].
1397pub struct TimestampingReconfiguration {
1398    /// Override for the existing [`mode`](TimestampingConfig::mode).
1399    pub mode: Maybe<Option<TimestampingMode>>,
1400    /// Override for the existing [`uncapped`](TimestampingConfig::uncapped) setting.
1401    pub uncapped: Maybe<Option<bool>>,
1402}
1403
1404impl TimestampingReconfiguration {
1405    /// Create a new [`TimestampingReconfiguration`].
1406    pub fn new() -> Self {
1407        Self::default()
1408    }
1409
1410    /// Set the override for the existing [`mode`](TimestampingConfig::mode).
1411    pub fn with_mode(self, mode: TimestampingMode) -> Self {
1412        Self {
1413            mode: Maybe::Specified(Some(mode)),
1414            ..self
1415        }
1416    }
1417
1418    /// Set the override for the existing [`uncapped`](TimestampingConfig::uncapped).
1419    pub fn with_uncapped(self, uncapped: bool) -> Self {
1420        Self {
1421            uncapped: Maybe::Specified(Some(uncapped)),
1422            ..self
1423        }
1424    }
1425}
1426
1427impl From<TimestampingReconfiguration> for api::config::TimestampingReconfiguration {
1428    fn from(value: TimestampingReconfiguration) -> Self {
1429        Self {
1430            mode: value.mode.map(|m| m.map(Into::into)),
1431            uncapped: value.uncapped,
1432        }
1433    }
1434}
1435
1436#[derive(Debug, Clone, Default)]
1437#[non_exhaustive]
1438/// Reconfiguration for [`DeleteOnEmptyConfig`].
1439pub struct DeleteOnEmptyReconfiguration {
1440    /// Override for the existing [`min_age_secs`](DeleteOnEmptyConfig::min_age_secs).
1441    pub min_age_secs: Maybe<Option<u64>>,
1442}
1443
1444impl DeleteOnEmptyReconfiguration {
1445    /// Create a new [`DeleteOnEmptyReconfiguration`].
1446    pub fn new() -> Self {
1447        Self::default()
1448    }
1449
1450    /// Set the override for the existing [`min_age_secs`](DeleteOnEmptyConfig::min_age_secs).
1451    pub fn with_min_age(self, min_age: Duration) -> Self {
1452        Self {
1453            min_age_secs: Maybe::Specified(Some(min_age.as_secs())),
1454        }
1455    }
1456}
1457
1458impl From<DeleteOnEmptyReconfiguration> for api::config::DeleteOnEmptyReconfiguration {
1459    fn from(value: DeleteOnEmptyReconfiguration) -> Self {
1460        Self {
1461            min_age_secs: value.min_age_secs,
1462        }
1463    }
1464}
1465
1466#[derive(Debug, Clone, Default)]
1467#[non_exhaustive]
1468/// Reconfiguration for [`StreamConfig`].
1469pub struct StreamReconfiguration {
1470    /// Override for the existing [`storage_class`](StreamConfig::storage_class).
1471    pub storage_class: Maybe<Option<StorageClass>>,
1472    /// Override for the existing [`retention_policy`](StreamConfig::retention_policy).
1473    pub retention_policy: Maybe<Option<RetentionPolicy>>,
1474    /// Override for the existing [`timestamping`](StreamConfig::timestamping).
1475    pub timestamping: Maybe<Option<TimestampingReconfiguration>>,
1476    /// Override for the existing [`delete_on_empty`](StreamConfig::delete_on_empty).
1477    pub delete_on_empty: Maybe<Option<DeleteOnEmptyReconfiguration>>,
1478}
1479
1480impl StreamReconfiguration {
1481    /// Create a new [`StreamReconfiguration`].
1482    pub fn new() -> Self {
1483        Self::default()
1484    }
1485
1486    /// Set the override for the existing [`storage_class`](StreamConfig::storage_class).
1487    pub fn with_storage_class(self, storage_class: StorageClass) -> Self {
1488        Self {
1489            storage_class: Maybe::Specified(Some(storage_class)),
1490            ..self
1491        }
1492    }
1493
1494    /// Set the override for the existing [`retention_policy`](StreamConfig::retention_policy).
1495    pub fn with_retention_policy(self, retention_policy: RetentionPolicy) -> Self {
1496        Self {
1497            retention_policy: Maybe::Specified(Some(retention_policy)),
1498            ..self
1499        }
1500    }
1501
1502    /// Set the override for the existing [`timestamping`](StreamConfig::timestamping).
1503    pub fn with_timestamping(self, timestamping: TimestampingReconfiguration) -> Self {
1504        Self {
1505            timestamping: Maybe::Specified(Some(timestamping)),
1506            ..self
1507        }
1508    }
1509
1510    /// Set the override for the existing [`delete_on_empty`](StreamConfig::delete_on_empty).
1511    pub fn with_delete_on_empty(self, delete_on_empty: DeleteOnEmptyReconfiguration) -> Self {
1512        Self {
1513            delete_on_empty: Maybe::Specified(Some(delete_on_empty)),
1514            ..self
1515        }
1516    }
1517}
1518
1519impl From<StreamReconfiguration> for api::config::StreamReconfiguration {
1520    fn from(value: StreamReconfiguration) -> Self {
1521        Self {
1522            storage_class: value.storage_class.map(|m| m.map(Into::into)),
1523            retention_policy: value.retention_policy.map(|m| m.map(Into::into)),
1524            timestamping: value.timestamping.map(|m| m.map(Into::into)),
1525            delete_on_empty: value.delete_on_empty.map(|m| m.map(Into::into)),
1526        }
1527    }
1528}
1529
1530#[derive(Debug, Clone, Default)]
1531#[non_exhaustive]
1532/// Reconfiguration for [`BasinConfig`].
1533pub struct BasinReconfiguration {
1534    /// Override for the existing [`default_stream_config`](BasinConfig::default_stream_config).
1535    pub default_stream_config: Maybe<Option<StreamReconfiguration>>,
1536    /// Override for the existing [`stream_cipher`](BasinConfig::stream_cipher).
1537    pub stream_cipher: Maybe<Option<EncryptionAlgorithm>>,
1538    /// Override for the existing
1539    /// [`create_stream_on_append`](BasinConfig::create_stream_on_append).
1540    pub create_stream_on_append: Maybe<bool>,
1541    /// Override for the existing [`create_stream_on_read`](BasinConfig::create_stream_on_read).
1542    pub create_stream_on_read: Maybe<bool>,
1543}
1544
1545impl BasinReconfiguration {
1546    /// Create a new [`BasinReconfiguration`].
1547    pub fn new() -> Self {
1548        Self::default()
1549    }
1550
1551    /// Set the override for the existing
1552    /// [`default_stream_config`](BasinConfig::default_stream_config).
1553    pub fn with_default_stream_config(self, config: StreamReconfiguration) -> Self {
1554        Self {
1555            default_stream_config: Maybe::Specified(Some(config)),
1556            ..self
1557        }
1558    }
1559
1560    /// Set the override for the existing [`stream_cipher`](BasinConfig::stream_cipher).
1561    pub fn with_stream_cipher(self, stream_cipher: EncryptionAlgorithm) -> Self {
1562        Self {
1563            stream_cipher: Maybe::Specified(Some(stream_cipher)),
1564            ..self
1565        }
1566    }
1567
1568    /// Set the override for the existing
1569    /// [`create_stream_on_append`](BasinConfig::create_stream_on_append).
1570    pub fn with_create_stream_on_append(self, create_stream_on_append: bool) -> Self {
1571        Self {
1572            create_stream_on_append: Maybe::Specified(create_stream_on_append),
1573            ..self
1574        }
1575    }
1576
1577    /// Set the override for the existing
1578    /// [`create_stream_on_read`](BasinConfig::create_stream_on_read).
1579    pub fn with_create_stream_on_read(self, create_stream_on_read: bool) -> Self {
1580        Self {
1581            create_stream_on_read: Maybe::Specified(create_stream_on_read),
1582            ..self
1583        }
1584    }
1585}
1586
1587impl From<BasinReconfiguration> for api::config::BasinReconfiguration {
1588    fn from(value: BasinReconfiguration) -> Self {
1589        Self {
1590            default_stream_config: value.default_stream_config.map(|m| m.map(Into::into)),
1591            stream_cipher: value.stream_cipher.map(|m| m.map(Into::into)),
1592            create_stream_on_append: value.create_stream_on_append,
1593            create_stream_on_read: value.create_stream_on_read,
1594        }
1595    }
1596}
1597
1598#[derive(Debug, Clone)]
1599#[non_exhaustive]
1600/// Input for [`reconfigure_basin`](crate::S2::reconfigure_basin) operation.
1601pub struct ReconfigureBasinInput {
1602    /// Basin name.
1603    pub name: BasinName,
1604    /// Reconfiguration for [`BasinConfig`].
1605    pub config: BasinReconfiguration,
1606}
1607
1608impl ReconfigureBasinInput {
1609    /// Create a new [`ReconfigureBasinInput`] with the given basin name and reconfiguration.
1610    pub fn new(name: BasinName, config: BasinReconfiguration) -> Self {
1611        Self { name, config }
1612    }
1613}
1614
1615#[derive(Debug, Clone, Default)]
1616#[non_exhaustive]
1617/// Input for [`list_access_tokens`](crate::S2::list_access_tokens) operation.
1618pub struct ListAccessTokensInput {
1619    /// Filter access tokens whose IDs begin with this value.
1620    ///
1621    /// Defaults to `""`.
1622    pub prefix: AccessTokenIdPrefix,
1623    /// Filter access tokens whose IDs are lexicographically greater than this value.
1624    ///
1625    /// Defaults to `""`.
1626    pub start_after: AccessTokenIdStartAfter,
1627    /// Number of access tokens to return in a page. Will be clamped to a maximum of `1000`.
1628    ///
1629    /// Defaults to `1000`.
1630    pub limit: Option<usize>,
1631}
1632
1633impl ListAccessTokensInput {
1634    /// Create a new [`ListAccessTokensInput`] with default values.
1635    pub fn new() -> Self {
1636        Self::default()
1637    }
1638
1639    /// Set the prefix used to filter access tokens whose IDs begin with this value.
1640    pub fn with_prefix(self, prefix: AccessTokenIdPrefix) -> Self {
1641        Self { prefix, ..self }
1642    }
1643
1644    /// Set the value used to filter access tokens whose IDs are lexicographically greater than this
1645    /// value.
1646    pub fn with_start_after(self, start_after: AccessTokenIdStartAfter) -> Self {
1647        Self {
1648            start_after,
1649            ..self
1650        }
1651    }
1652
1653    /// Set the limit on number of access tokens to return in a page.
1654    pub fn with_limit(self, limit: usize) -> Self {
1655        Self {
1656            limit: Some(limit),
1657            ..self
1658        }
1659    }
1660}
1661
1662impl From<ListAccessTokensInput> for api::access::ListAccessTokensRequest {
1663    fn from(value: ListAccessTokensInput) -> Self {
1664        Self {
1665            prefix: Some(value.prefix),
1666            start_after: Some(value.start_after),
1667            limit: value.limit,
1668        }
1669    }
1670}
1671
1672#[derive(Debug, Clone, Default)]
1673/// Input for [`list_all_access_tokens`](crate::S2::list_all_access_tokens) operation.
1674pub struct ListAllAccessTokensInput {
1675    /// Filter access tokens whose IDs begin with this value.
1676    ///
1677    /// Defaults to `""`.
1678    pub prefix: AccessTokenIdPrefix,
1679    /// Filter access tokens whose IDs are lexicographically greater than this value.
1680    ///
1681    /// Defaults to `""`.
1682    pub start_after: AccessTokenIdStartAfter,
1683}
1684
1685impl ListAllAccessTokensInput {
1686    /// Create a new [`ListAllAccessTokensInput`] with default values.
1687    pub fn new() -> Self {
1688        Self::default()
1689    }
1690
1691    /// Set the prefix used to filter access tokens whose IDs begin with this value.
1692    pub fn with_prefix(self, prefix: AccessTokenIdPrefix) -> Self {
1693        Self { prefix, ..self }
1694    }
1695
1696    /// Set the value used to filter access tokens whose IDs are lexicographically greater than
1697    /// this value.
1698    pub fn with_start_after(self, start_after: AccessTokenIdStartAfter) -> Self {
1699        Self {
1700            start_after,
1701            ..self
1702        }
1703    }
1704}
1705
1706#[derive(Debug, Clone, PartialEq, Eq)]
1707#[non_exhaustive]
1708/// Location information.
1709pub struct LocationInfo {
1710    /// Location name.
1711    pub name: LocationName,
1712    /// Location represents a private placement, limited by account.
1713    pub is_private: bool,
1714}
1715
1716impl From<api::location::LocationInfo> for LocationInfo {
1717    fn from(value: api::location::LocationInfo) -> Self {
1718        Self {
1719            name: value.name,
1720            is_private: value.is_private,
1721        }
1722    }
1723}
1724
1725#[derive(Debug, Clone)]
1726#[non_exhaustive]
1727/// Access token information.
1728pub struct AccessTokenInfo {
1729    /// Access token ID.
1730    pub id: AccessTokenId,
1731    /// Expiration time, or `None` if the token does not expire.
1732    pub expires_at: Option<S2DateTime>,
1733    /// Whether to automatically prefix stream names during creation and strip the prefix during
1734    /// listing.
1735    pub auto_prefix_streams: bool,
1736    /// Scope of the access token.
1737    pub scope: AccessTokenScope,
1738}
1739
1740impl TryFrom<api::access::AccessTokenInfo> for AccessTokenInfo {
1741    type Error = ValidationError;
1742
1743    fn try_from(value: api::access::AccessTokenInfo) -> Result<Self, Self::Error> {
1744        let expires_at = value.expires_at.map(S2DateTime::try_from).transpose()?;
1745        Ok(Self {
1746            id: value.id,
1747            expires_at,
1748            auto_prefix_streams: value.auto_prefix_streams,
1749            scope: value.scope.into(),
1750        })
1751    }
1752}
1753
1754#[derive(Debug, Clone)]
1755/// Pattern for matching basins.
1756///
1757/// See [`AccessTokenScope::basins`].
1758pub enum BasinMatcher {
1759    /// Match no basins.
1760    None,
1761    /// Match exactly this basin.
1762    Exact(BasinName),
1763    /// Match all basins with this prefix.
1764    Prefix(BasinNamePrefix),
1765}
1766
1767#[derive(Debug, Clone)]
1768/// Pattern for matching streams.
1769///
1770/// See [`AccessTokenScope::streams`].
1771pub enum StreamMatcher {
1772    /// Match no streams.
1773    None,
1774    /// Match exactly this stream.
1775    Exact(StreamName),
1776    /// Match all streams with this prefix.
1777    Prefix(StreamNamePrefix),
1778}
1779
1780#[derive(Debug, Clone)]
1781/// Pattern for matching access tokens.
1782///
1783/// See [`AccessTokenScope::access_tokens`].
1784pub enum AccessTokenMatcher {
1785    /// Match no access tokens.
1786    None,
1787    /// Match exactly this access token.
1788    Exact(AccessTokenId),
1789    /// Match all access tokens with this prefix.
1790    Prefix(AccessTokenIdPrefix),
1791}
1792
1793#[derive(Debug, Clone, Default)]
1794#[non_exhaustive]
1795/// Permissions indicating allowed operations.
1796pub struct ReadWritePermissions {
1797    /// Read permission.
1798    ///
1799    /// Defaults to `false`.
1800    pub read: bool,
1801    /// Write permission.
1802    ///
1803    /// Defaults to `false`.
1804    pub write: bool,
1805}
1806
1807impl ReadWritePermissions {
1808    /// Create a new [`ReadWritePermissions`] with default values.
1809    pub fn new() -> Self {
1810        Self::default()
1811    }
1812
1813    /// Create read-only permissions.
1814    pub fn read_only() -> Self {
1815        Self {
1816            read: true,
1817            write: false,
1818        }
1819    }
1820
1821    /// Create write-only permissions.
1822    pub fn write_only() -> Self {
1823        Self {
1824            read: false,
1825            write: true,
1826        }
1827    }
1828
1829    /// Create read-write permissions.
1830    pub fn read_write() -> Self {
1831        Self {
1832            read: true,
1833            write: true,
1834        }
1835    }
1836}
1837
1838impl From<ReadWritePermissions> for api::access::ReadWritePermissions {
1839    fn from(value: ReadWritePermissions) -> Self {
1840        Self {
1841            read: Some(value.read),
1842            write: Some(value.write),
1843        }
1844    }
1845}
1846
1847impl From<api::access::ReadWritePermissions> for ReadWritePermissions {
1848    fn from(value: api::access::ReadWritePermissions) -> Self {
1849        Self {
1850            read: value.read.unwrap_or_default(),
1851            write: value.write.unwrap_or_default(),
1852        }
1853    }
1854}
1855
1856#[derive(Debug, Clone, Default)]
1857#[non_exhaustive]
1858/// Permissions at the operation group level.
1859///
1860/// See [`AccessTokenScope::op_group_perms`].
1861pub struct OperationGroupPermissions {
1862    /// Account-level access permissions.
1863    ///
1864    /// Defaults to `None`.
1865    pub account: Option<ReadWritePermissions>,
1866    /// Basin-level access permissions.
1867    ///
1868    /// Defaults to `None`.
1869    pub basin: Option<ReadWritePermissions>,
1870    /// Stream-level access permissions.
1871    ///
1872    /// Defaults to `None`.
1873    pub stream: Option<ReadWritePermissions>,
1874}
1875
1876impl OperationGroupPermissions {
1877    /// Create a new [`OperationGroupPermissions`] with default values.
1878    pub fn new() -> Self {
1879        Self::default()
1880    }
1881
1882    /// Create read-only permissions for all groups.
1883    pub fn read_only_all() -> Self {
1884        Self {
1885            account: Some(ReadWritePermissions::read_only()),
1886            basin: Some(ReadWritePermissions::read_only()),
1887            stream: Some(ReadWritePermissions::read_only()),
1888        }
1889    }
1890
1891    /// Create write-only permissions for all groups.
1892    pub fn write_only_all() -> Self {
1893        Self {
1894            account: Some(ReadWritePermissions::write_only()),
1895            basin: Some(ReadWritePermissions::write_only()),
1896            stream: Some(ReadWritePermissions::write_only()),
1897        }
1898    }
1899
1900    /// Create read-write permissions for all groups.
1901    pub fn read_write_all() -> Self {
1902        Self {
1903            account: Some(ReadWritePermissions::read_write()),
1904            basin: Some(ReadWritePermissions::read_write()),
1905            stream: Some(ReadWritePermissions::read_write()),
1906        }
1907    }
1908
1909    /// Set account-level access permissions.
1910    pub fn with_account(self, account: ReadWritePermissions) -> Self {
1911        Self {
1912            account: Some(account),
1913            ..self
1914        }
1915    }
1916
1917    /// Set basin-level access permissions.
1918    pub fn with_basin(self, basin: ReadWritePermissions) -> Self {
1919        Self {
1920            basin: Some(basin),
1921            ..self
1922        }
1923    }
1924
1925    /// Set stream-level access permissions.
1926    pub fn with_stream(self, stream: ReadWritePermissions) -> Self {
1927        Self {
1928            stream: Some(stream),
1929            ..self
1930        }
1931    }
1932}
1933
1934impl From<OperationGroupPermissions> for api::access::PermittedOperationGroups {
1935    fn from(value: OperationGroupPermissions) -> Self {
1936        Self {
1937            account: value.account.map(Into::into),
1938            basin: value.basin.map(Into::into),
1939            stream: value.stream.map(Into::into),
1940        }
1941    }
1942}
1943
1944impl From<api::access::PermittedOperationGroups> for OperationGroupPermissions {
1945    fn from(value: api::access::PermittedOperationGroups) -> Self {
1946        Self {
1947            account: value.account.map(Into::into),
1948            basin: value.basin.map(Into::into),
1949            stream: value.stream.map(Into::into),
1950        }
1951    }
1952}
1953
1954#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
1955/// Individual operation that can be permitted.
1956///
1957/// See [`AccessTokenScope::ops`].
1958pub enum Operation {
1959    /// List basins.
1960    ListBasins,
1961    /// Create a basin.
1962    CreateBasin,
1963    /// Get basin configuration.
1964    GetBasinConfig,
1965    /// Delete a basin.
1966    DeleteBasin,
1967    /// Reconfigure a basin.
1968    ReconfigureBasin,
1969    /// List access tokens.
1970    ListAccessTokens,
1971    /// Issue an access token.
1972    IssueAccessToken,
1973    /// Revoke an access token.
1974    RevokeAccessToken,
1975    /// Get account metrics.
1976    GetAccountMetrics,
1977    /// Get basin metrics.
1978    GetBasinMetrics,
1979    /// Get stream metrics.
1980    GetStreamMetrics,
1981    /// List streams.
1982    ListStreams,
1983    /// Create a stream.
1984    CreateStream,
1985    /// Get stream configuration.
1986    GetStreamConfig,
1987    /// Delete a stream.
1988    DeleteStream,
1989    /// Reconfigure a stream.
1990    ReconfigureStream,
1991    /// Check the tail of a stream.
1992    CheckTail,
1993    /// Append records to a stream.
1994    Append,
1995    /// Read records from a stream.
1996    Read,
1997    /// Trim records on a stream.
1998    Trim,
1999    /// Set the fencing token on a stream.
2000    Fence,
2001    /// List locations.
2002    ListLocations,
2003    /// Get the default location.
2004    GetDefaultLocation,
2005    /// Set the default location.
2006    SetDefaultLocation,
2007}
2008
2009impl From<Operation> for api::access::Operation {
2010    fn from(value: Operation) -> Self {
2011        match value {
2012            Operation::ListBasins => api::access::Operation::ListBasins,
2013            Operation::CreateBasin => api::access::Operation::CreateBasin,
2014            Operation::DeleteBasin => api::access::Operation::DeleteBasin,
2015            Operation::ReconfigureBasin => api::access::Operation::ReconfigureBasin,
2016            Operation::GetBasinConfig => api::access::Operation::GetBasinConfig,
2017            Operation::IssueAccessToken => api::access::Operation::IssueAccessToken,
2018            Operation::RevokeAccessToken => api::access::Operation::RevokeAccessToken,
2019            Operation::ListAccessTokens => api::access::Operation::ListAccessTokens,
2020            Operation::ListStreams => api::access::Operation::ListStreams,
2021            Operation::CreateStream => api::access::Operation::CreateStream,
2022            Operation::DeleteStream => api::access::Operation::DeleteStream,
2023            Operation::GetStreamConfig => api::access::Operation::GetStreamConfig,
2024            Operation::ReconfigureStream => api::access::Operation::ReconfigureStream,
2025            Operation::CheckTail => api::access::Operation::CheckTail,
2026            Operation::Append => api::access::Operation::Append,
2027            Operation::Read => api::access::Operation::Read,
2028            Operation::Trim => api::access::Operation::Trim,
2029            Operation::Fence => api::access::Operation::Fence,
2030            Operation::GetAccountMetrics => api::access::Operation::AccountMetrics,
2031            Operation::GetBasinMetrics => api::access::Operation::BasinMetrics,
2032            Operation::GetStreamMetrics => api::access::Operation::StreamMetrics,
2033            Operation::ListLocations => api::access::Operation::ListLocations,
2034            Operation::GetDefaultLocation => api::access::Operation::GetDefaultLocation,
2035            Operation::SetDefaultLocation => api::access::Operation::SetDefaultLocation,
2036        }
2037    }
2038}
2039
2040impl From<api::access::Operation> for Operation {
2041    fn from(value: api::access::Operation) -> Self {
2042        match value {
2043            api::access::Operation::ListBasins => Operation::ListBasins,
2044            api::access::Operation::CreateBasin => Operation::CreateBasin,
2045            api::access::Operation::DeleteBasin => Operation::DeleteBasin,
2046            api::access::Operation::ReconfigureBasin => Operation::ReconfigureBasin,
2047            api::access::Operation::GetBasinConfig => Operation::GetBasinConfig,
2048            api::access::Operation::IssueAccessToken => Operation::IssueAccessToken,
2049            api::access::Operation::RevokeAccessToken => Operation::RevokeAccessToken,
2050            api::access::Operation::ListAccessTokens => Operation::ListAccessTokens,
2051            api::access::Operation::ListStreams => Operation::ListStreams,
2052            api::access::Operation::CreateStream => Operation::CreateStream,
2053            api::access::Operation::DeleteStream => Operation::DeleteStream,
2054            api::access::Operation::GetStreamConfig => Operation::GetStreamConfig,
2055            api::access::Operation::ReconfigureStream => Operation::ReconfigureStream,
2056            api::access::Operation::CheckTail => Operation::CheckTail,
2057            api::access::Operation::Append => Operation::Append,
2058            api::access::Operation::Read => Operation::Read,
2059            api::access::Operation::Trim => Operation::Trim,
2060            api::access::Operation::Fence => Operation::Fence,
2061            api::access::Operation::AccountMetrics => Operation::GetAccountMetrics,
2062            api::access::Operation::BasinMetrics => Operation::GetBasinMetrics,
2063            api::access::Operation::StreamMetrics => Operation::GetStreamMetrics,
2064            api::access::Operation::ListLocations => Operation::ListLocations,
2065            api::access::Operation::GetDefaultLocation => Operation::GetDefaultLocation,
2066            api::access::Operation::SetDefaultLocation => Operation::SetDefaultLocation,
2067        }
2068    }
2069}
2070
2071#[derive(Debug, Clone)]
2072#[non_exhaustive]
2073/// Scope of an access token.
2074///
2075/// **Note:** The final set of permitted operations is the union of [`ops`](AccessTokenScope::ops)
2076/// and the operations permitted by [`op_group_perms`](AccessTokenScope::op_group_perms). Also, the
2077/// final set must not be empty.
2078///
2079/// See [`IssueAccessTokenInput::scope`].
2080pub struct AccessTokenScopeInput {
2081    basins: Option<BasinMatcher>,
2082    streams: Option<StreamMatcher>,
2083    access_tokens: Option<AccessTokenMatcher>,
2084    op_group_perms: Option<OperationGroupPermissions>,
2085    ops: HashSet<Operation>,
2086}
2087
2088impl AccessTokenScopeInput {
2089    /// Create a new [`AccessTokenScopeInput`] with the given permitted operations.
2090    pub fn from_ops(ops: impl IntoIterator<Item = Operation>) -> Self {
2091        Self {
2092            basins: None,
2093            streams: None,
2094            access_tokens: None,
2095            op_group_perms: None,
2096            ops: ops.into_iter().collect(),
2097        }
2098    }
2099
2100    /// Create a new [`AccessTokenScopeInput`] with the given operation group permissions.
2101    pub fn from_op_group_perms(op_group_perms: OperationGroupPermissions) -> Self {
2102        Self {
2103            basins: None,
2104            streams: None,
2105            access_tokens: None,
2106            op_group_perms: Some(op_group_perms),
2107            ops: HashSet::default(),
2108        }
2109    }
2110
2111    /// Set the permitted operations.
2112    pub fn with_ops(self, ops: impl IntoIterator<Item = Operation>) -> Self {
2113        Self {
2114            ops: ops.into_iter().collect(),
2115            ..self
2116        }
2117    }
2118
2119    /// Set the access permissions at the operation group level.
2120    pub fn with_op_group_perms(self, op_group_perms: OperationGroupPermissions) -> Self {
2121        Self {
2122            op_group_perms: Some(op_group_perms),
2123            ..self
2124        }
2125    }
2126
2127    /// Set the permitted basins.
2128    ///
2129    /// Defaults to no basins.
2130    pub fn with_basins(self, basins: BasinMatcher) -> Self {
2131        Self {
2132            basins: Some(basins),
2133            ..self
2134        }
2135    }
2136
2137    /// Set the permitted streams.
2138    ///
2139    /// Defaults to no streams.
2140    pub fn with_streams(self, streams: StreamMatcher) -> Self {
2141        Self {
2142            streams: Some(streams),
2143            ..self
2144        }
2145    }
2146
2147    /// Set the permitted access tokens.
2148    ///
2149    /// Defaults to no access tokens.
2150    pub fn with_access_tokens(self, access_tokens: AccessTokenMatcher) -> Self {
2151        Self {
2152            access_tokens: Some(access_tokens),
2153            ..self
2154        }
2155    }
2156}
2157
2158#[derive(Debug, Clone)]
2159#[non_exhaustive]
2160/// Scope of an access token.
2161pub struct AccessTokenScope {
2162    /// Permitted basins.
2163    pub basins: Option<BasinMatcher>,
2164    /// Permitted streams.
2165    pub streams: Option<StreamMatcher>,
2166    /// Permitted access tokens.
2167    pub access_tokens: Option<AccessTokenMatcher>,
2168    /// Permissions at the operation group level.
2169    pub op_group_perms: Option<OperationGroupPermissions>,
2170    /// Permitted operations.
2171    pub ops: HashSet<Operation>,
2172}
2173
2174impl From<api::access::AccessTokenScope> for AccessTokenScope {
2175    fn from(value: api::access::AccessTokenScope) -> Self {
2176        Self {
2177            basins: value.basins.map(|rs| match rs {
2178                api::access::ResourceSet::Exact(api::access::MaybeEmpty::NonEmpty(e)) => {
2179                    BasinMatcher::Exact(e)
2180                }
2181                api::access::ResourceSet::Exact(api::access::MaybeEmpty::Empty) => {
2182                    BasinMatcher::None
2183                }
2184                api::access::ResourceSet::Prefix(p) => BasinMatcher::Prefix(p),
2185            }),
2186            streams: value.streams.map(|rs| match rs {
2187                api::access::ResourceSet::Exact(api::access::MaybeEmpty::NonEmpty(e)) => {
2188                    StreamMatcher::Exact(e)
2189                }
2190                api::access::ResourceSet::Exact(api::access::MaybeEmpty::Empty) => {
2191                    StreamMatcher::None
2192                }
2193                api::access::ResourceSet::Prefix(p) => StreamMatcher::Prefix(p),
2194            }),
2195            access_tokens: value.access_tokens.map(|rs| match rs {
2196                api::access::ResourceSet::Exact(api::access::MaybeEmpty::NonEmpty(e)) => {
2197                    AccessTokenMatcher::Exact(e)
2198                }
2199                api::access::ResourceSet::Exact(api::access::MaybeEmpty::Empty) => {
2200                    AccessTokenMatcher::None
2201                }
2202                api::access::ResourceSet::Prefix(p) => AccessTokenMatcher::Prefix(p),
2203            }),
2204            op_group_perms: value.op_groups.map(Into::into),
2205            ops: value
2206                .ops
2207                .map(|ops| ops.into_iter().map(Into::into).collect())
2208                .unwrap_or_default(),
2209        }
2210    }
2211}
2212
2213impl From<AccessTokenScopeInput> for api::access::AccessTokenScope {
2214    fn from(value: AccessTokenScopeInput) -> Self {
2215        Self {
2216            basins: value.basins.map(|rs| match rs {
2217                BasinMatcher::None => {
2218                    api::access::ResourceSet::Exact(api::access::MaybeEmpty::Empty)
2219                }
2220                BasinMatcher::Exact(e) => {
2221                    api::access::ResourceSet::Exact(api::access::MaybeEmpty::NonEmpty(e))
2222                }
2223                BasinMatcher::Prefix(p) => api::access::ResourceSet::Prefix(p),
2224            }),
2225            streams: value.streams.map(|rs| match rs {
2226                StreamMatcher::None => {
2227                    api::access::ResourceSet::Exact(api::access::MaybeEmpty::Empty)
2228                }
2229                StreamMatcher::Exact(e) => {
2230                    api::access::ResourceSet::Exact(api::access::MaybeEmpty::NonEmpty(e))
2231                }
2232                StreamMatcher::Prefix(p) => api::access::ResourceSet::Prefix(p),
2233            }),
2234            access_tokens: value.access_tokens.map(|rs| match rs {
2235                AccessTokenMatcher::None => {
2236                    api::access::ResourceSet::Exact(api::access::MaybeEmpty::Empty)
2237                }
2238                AccessTokenMatcher::Exact(e) => {
2239                    api::access::ResourceSet::Exact(api::access::MaybeEmpty::NonEmpty(e))
2240                }
2241                AccessTokenMatcher::Prefix(p) => api::access::ResourceSet::Prefix(p),
2242            }),
2243            op_groups: value.op_group_perms.map(Into::into),
2244            ops: if value.ops.is_empty() {
2245                None
2246            } else {
2247                Some(value.ops.into_iter().map(Into::into).collect())
2248            },
2249        }
2250    }
2251}
2252
2253#[derive(Debug, Clone)]
2254#[non_exhaustive]
2255/// Input for [`issue_access_token`](crate::S2::issue_access_token).
2256pub struct IssueAccessTokenInput {
2257    /// Access token ID.
2258    pub id: AccessTokenId,
2259    /// Expiration time.
2260    ///
2261    /// Defaults to the expiration time of requestor's access token passed via
2262    /// [`S2Config`](S2Config::new).
2263    pub expires_at: Option<S2DateTime>,
2264    /// Whether to automatically prefix stream names during creation and strip the prefix during
2265    /// listing.
2266    ///
2267    /// **Note:** [`scope.streams`](AccessTokenScopeInput::with_streams) must be set with the
2268    /// prefix.
2269    ///
2270    /// Defaults to `false`.
2271    pub auto_prefix_streams: bool,
2272    /// Scope of the token.
2273    pub scope: AccessTokenScopeInput,
2274}
2275
2276impl IssueAccessTokenInput {
2277    /// Create a new [`IssueAccessTokenInput`] with the given id and scope.
2278    pub fn new(id: AccessTokenId, scope: AccessTokenScopeInput) -> Self {
2279        Self {
2280            id,
2281            expires_at: None,
2282            auto_prefix_streams: false,
2283            scope,
2284        }
2285    }
2286
2287    /// Set the expiration time.
2288    pub fn with_expires_at(self, expires_at: S2DateTime) -> Self {
2289        Self {
2290            expires_at: Some(expires_at),
2291            ..self
2292        }
2293    }
2294
2295    /// Set whether to automatically prefix stream names during creation and strip the prefix during
2296    /// listing.
2297    pub fn with_auto_prefix_streams(self, auto_prefix_streams: bool) -> Self {
2298        Self {
2299            auto_prefix_streams,
2300            ..self
2301        }
2302    }
2303}
2304
2305impl From<IssueAccessTokenInput> for api::access::IssueAccessTokenRequest {
2306    fn from(value: IssueAccessTokenInput) -> Self {
2307        Self {
2308            id: value.id,
2309            expires_at: value.expires_at.map(Into::into),
2310            auto_prefix_streams: value.auto_prefix_streams.then_some(true),
2311            scope: value.scope.into(),
2312        }
2313    }
2314}
2315
2316#[derive(Debug, Clone, Copy, PartialEq, Eq)]
2317/// Interval to accumulate over for timeseries metric sets.
2318pub enum TimeseriesInterval {
2319    /// Minute.
2320    Minute,
2321    /// Hour.
2322    Hour,
2323    /// Day.
2324    Day,
2325}
2326
2327impl From<TimeseriesInterval> for api::metrics::TimeseriesInterval {
2328    fn from(value: TimeseriesInterval) -> Self {
2329        match value {
2330            TimeseriesInterval::Minute => api::metrics::TimeseriesInterval::Minute,
2331            TimeseriesInterval::Hour => api::metrics::TimeseriesInterval::Hour,
2332            TimeseriesInterval::Day => api::metrics::TimeseriesInterval::Day,
2333        }
2334    }
2335}
2336
2337impl From<api::metrics::TimeseriesInterval> for TimeseriesInterval {
2338    fn from(value: api::metrics::TimeseriesInterval) -> Self {
2339        match value {
2340            api::metrics::TimeseriesInterval::Minute => TimeseriesInterval::Minute,
2341            api::metrics::TimeseriesInterval::Hour => TimeseriesInterval::Hour,
2342            api::metrics::TimeseriesInterval::Day => TimeseriesInterval::Day,
2343        }
2344    }
2345}
2346
2347#[derive(Debug, Clone, Copy)]
2348#[non_exhaustive]
2349/// Time range as Unix epoch seconds.
2350pub struct TimeRange {
2351    /// Start timestamp (inclusive).
2352    pub start: u32,
2353    /// End timestamp (exclusive).
2354    pub end: u32,
2355}
2356
2357impl TimeRange {
2358    /// Create a new [`TimeRange`] with the given start and end timestamps.
2359    pub fn new(start: u32, end: u32) -> Self {
2360        Self { start, end }
2361    }
2362}
2363
2364#[derive(Debug, Clone, Copy)]
2365#[non_exhaustive]
2366/// Time range as Unix epoch seconds and accumulation interval.
2367pub struct TimeRangeAndInterval {
2368    /// Start timestamp (inclusive).
2369    pub start: u32,
2370    /// End timestamp (exclusive).
2371    pub end: u32,
2372    /// Interval to accumulate over for timeseries metric sets.
2373    ///
2374    /// Default is dependent on the requested metric set.
2375    pub interval: Option<TimeseriesInterval>,
2376}
2377
2378impl TimeRangeAndInterval {
2379    /// Create a new [`TimeRangeAndInterval`] with the given start and end timestamps.
2380    pub fn new(start: u32, end: u32) -> Self {
2381        Self {
2382            start,
2383            end,
2384            interval: None,
2385        }
2386    }
2387
2388    /// Set the interval to accumulate over for timeseries metric sets.
2389    pub fn with_interval(self, interval: TimeseriesInterval) -> Self {
2390        Self {
2391            interval: Some(interval),
2392            ..self
2393        }
2394    }
2395}
2396
2397#[derive(Debug, Clone, Copy)]
2398/// Account metric set to return.
2399pub enum AccountMetricSet {
2400    /// Returns a [`LabelMetric`] representing all basins which had at least one stream within the
2401    /// specified time range.
2402    ActiveBasins(TimeRange),
2403    /// Returns [`AccumulationMetric`]s, one per account operation type.
2404    ///
2405    /// Each metric represents a timeseries of the number of operations, with one accumulated value
2406    /// per interval over the requested time range.
2407    ///
2408    /// [`interval`](TimeRangeAndInterval::interval) defaults to [`hour`](TimeseriesInterval::Hour).
2409    AccountOps(TimeRangeAndInterval),
2410}
2411
2412#[derive(Debug, Clone)]
2413#[non_exhaustive]
2414/// Input for [`get_account_metrics`](crate::S2::get_account_metrics) operation.
2415pub struct GetAccountMetricsInput {
2416    /// Metric set to return.
2417    pub set: AccountMetricSet,
2418}
2419
2420impl GetAccountMetricsInput {
2421    /// Create a new [`GetAccountMetricsInput`] with the given account metric set.
2422    pub fn new(set: AccountMetricSet) -> Self {
2423        Self { set }
2424    }
2425}
2426
2427impl From<GetAccountMetricsInput> for api::metrics::AccountMetricSetRequest {
2428    fn from(value: GetAccountMetricsInput) -> Self {
2429        let (set, start, end, interval) = match value.set {
2430            AccountMetricSet::ActiveBasins(args) => (
2431                api::metrics::AccountMetricSet::ActiveBasins,
2432                args.start,
2433                args.end,
2434                None,
2435            ),
2436            AccountMetricSet::AccountOps(args) => (
2437                api::metrics::AccountMetricSet::AccountOps,
2438                args.start,
2439                args.end,
2440                args.interval,
2441            ),
2442        };
2443        Self {
2444            set,
2445            start: Some(start),
2446            end: Some(end),
2447            interval: interval.map(Into::into),
2448        }
2449    }
2450}
2451
2452#[derive(Debug, Clone, Copy)]
2453/// Basin metric set to return.
2454pub enum BasinMetricSet {
2455    /// Returns a [`GaugeMetric`] representing a timeseries of total stored bytes across all streams
2456    /// in the basin, with one observed value for each hour over the requested time range.
2457    Storage(TimeRange),
2458    /// Returns [`AccumulationMetric`]s, one per storage class (standard, express).
2459    ///
2460    /// Each metric represents a timeseries of the number of append operations across all streams
2461    /// in the basin, with one accumulated value per interval over the requested time range.
2462    ///
2463    /// [`interval`](TimeRangeAndInterval::interval) defaults to
2464    /// [`minute`](TimeseriesInterval::Minute).
2465    AppendOps(TimeRangeAndInterval),
2466    /// Returns [`AccumulationMetric`]s, one per read type (unary, streaming).
2467    ///
2468    /// Each metric represents a timeseries of the number of read operations across all streams
2469    /// in the basin, with one accumulated value per interval over the requested time range.
2470    ///
2471    /// [`interval`](TimeRangeAndInterval::interval) defaults to
2472    /// [`minute`](TimeseriesInterval::Minute).
2473    ReadOps(TimeRangeAndInterval),
2474    /// Returns an [`AccumulationMetric`] representing a timeseries of total read bytes
2475    /// across all streams in the basin, with one accumulated value per interval
2476    /// over the requested time range.
2477    ///
2478    /// [`interval`](TimeRangeAndInterval::interval) defaults to
2479    /// [`minute`](TimeseriesInterval::Minute).
2480    ReadThroughput(TimeRangeAndInterval),
2481    /// Returns an [`AccumulationMetric`] representing a timeseries of total appended bytes
2482    /// across all streams in the basin, with one accumulated value per interval
2483    /// over the requested time range.
2484    ///
2485    /// [`interval`](TimeRangeAndInterval::interval) defaults to
2486    /// [`minute`](TimeseriesInterval::Minute).
2487    AppendThroughput(TimeRangeAndInterval),
2488    /// Returns [`AccumulationMetric`]s, one per basin operation type.
2489    ///
2490    /// Each metric represents a timeseries of the number of operations, with one accumulated value
2491    /// per interval over the requested time range.
2492    ///
2493    /// [`interval`](TimeRangeAndInterval::interval) defaults to [`hour`](TimeseriesInterval::Hour).
2494    BasinOps(TimeRangeAndInterval),
2495}
2496
2497#[derive(Debug, Clone)]
2498#[non_exhaustive]
2499/// Input for [`get_basin_metrics`](crate::S2::get_basin_metrics) operation.
2500pub struct GetBasinMetricsInput {
2501    /// Basin name.
2502    pub name: BasinName,
2503    /// Metric set to return.
2504    pub set: BasinMetricSet,
2505}
2506
2507impl GetBasinMetricsInput {
2508    /// Create a new [`GetBasinMetricsInput`] with the given basin name and metric set.
2509    pub fn new(name: BasinName, set: BasinMetricSet) -> Self {
2510        Self { name, set }
2511    }
2512}
2513
2514impl From<GetBasinMetricsInput> for (BasinName, api::metrics::BasinMetricSetRequest) {
2515    fn from(value: GetBasinMetricsInput) -> Self {
2516        let (set, start, end, interval) = match value.set {
2517            BasinMetricSet::Storage(args) => (
2518                api::metrics::BasinMetricSet::Storage,
2519                args.start,
2520                args.end,
2521                None,
2522            ),
2523            BasinMetricSet::AppendOps(args) => (
2524                api::metrics::BasinMetricSet::AppendOps,
2525                args.start,
2526                args.end,
2527                args.interval,
2528            ),
2529            BasinMetricSet::ReadOps(args) => (
2530                api::metrics::BasinMetricSet::ReadOps,
2531                args.start,
2532                args.end,
2533                args.interval,
2534            ),
2535            BasinMetricSet::ReadThroughput(args) => (
2536                api::metrics::BasinMetricSet::ReadThroughput,
2537                args.start,
2538                args.end,
2539                args.interval,
2540            ),
2541            BasinMetricSet::AppendThroughput(args) => (
2542                api::metrics::BasinMetricSet::AppendThroughput,
2543                args.start,
2544                args.end,
2545                args.interval,
2546            ),
2547            BasinMetricSet::BasinOps(args) => (
2548                api::metrics::BasinMetricSet::BasinOps,
2549                args.start,
2550                args.end,
2551                args.interval,
2552            ),
2553        };
2554        (
2555            value.name,
2556            api::metrics::BasinMetricSetRequest {
2557                set,
2558                start: Some(start),
2559                end: Some(end),
2560                interval: interval.map(Into::into),
2561            },
2562        )
2563    }
2564}
2565
2566#[derive(Debug, Clone, Copy)]
2567/// Stream metric set to return.
2568pub enum StreamMetricSet {
2569    /// Returns a [`GaugeMetric`] representing a timeseries of total stored bytes for the stream,
2570    /// with one observed value for each minute over the requested time range.
2571    Storage(TimeRange),
2572}
2573
2574#[derive(Debug, Clone)]
2575#[non_exhaustive]
2576/// Input for [`get_stream_metrics`](crate::S2::get_stream_metrics) operation.
2577pub struct GetStreamMetricsInput {
2578    /// Basin name.
2579    pub basin_name: BasinName,
2580    /// Stream name.
2581    pub stream_name: StreamName,
2582    /// Metric set to return.
2583    pub set: StreamMetricSet,
2584}
2585
2586impl GetStreamMetricsInput {
2587    /// Create a new [`GetStreamMetricsInput`] with the given basin name, stream name and metric
2588    /// set.
2589    pub fn new(basin_name: BasinName, stream_name: StreamName, set: StreamMetricSet) -> Self {
2590        Self {
2591            basin_name,
2592            stream_name,
2593            set,
2594        }
2595    }
2596}
2597
2598impl From<GetStreamMetricsInput> for (BasinName, StreamName, api::metrics::StreamMetricSetRequest) {
2599    fn from(value: GetStreamMetricsInput) -> Self {
2600        let (set, start, end, interval) = match value.set {
2601            StreamMetricSet::Storage(args) => (
2602                api::metrics::StreamMetricSet::Storage,
2603                args.start,
2604                args.end,
2605                None,
2606            ),
2607        };
2608        (
2609            value.basin_name,
2610            value.stream_name,
2611            api::metrics::StreamMetricSetRequest {
2612                set,
2613                start: Some(start),
2614                end: Some(end),
2615                interval,
2616            },
2617        )
2618    }
2619}
2620
2621#[derive(Debug, Clone, Copy, PartialEq, Eq)]
2622/// Unit in which metric values are measured.
2623pub enum MetricUnit {
2624    /// Size in bytes.
2625    Bytes,
2626    /// Number of operations.
2627    Operations,
2628}
2629
2630impl From<api::metrics::MetricUnit> for MetricUnit {
2631    fn from(value: api::metrics::MetricUnit) -> Self {
2632        match value {
2633            api::metrics::MetricUnit::Bytes => MetricUnit::Bytes,
2634            api::metrics::MetricUnit::Operations => MetricUnit::Operations,
2635        }
2636    }
2637}
2638
2639#[derive(Debug, Clone)]
2640#[non_exhaustive]
2641/// Single named value.
2642pub struct ScalarMetric {
2643    /// Metric name.
2644    pub name: String,
2645    /// Unit for the metric value.
2646    pub unit: MetricUnit,
2647    /// Metric value.
2648    pub value: f64,
2649}
2650
2651#[derive(Debug, Clone)]
2652#[non_exhaustive]
2653/// Named series of `(timestamp, value)` datapoints, each representing an accumulation over a
2654/// specified interval.
2655pub struct AccumulationMetric {
2656    /// Timeseries name.
2657    pub name: String,
2658    /// Unit for the accumulated values.
2659    pub unit: MetricUnit,
2660    /// The interval at which datapoints are accumulated.
2661    pub interval: TimeseriesInterval,
2662    /// Series of `(timestamp, value)` datapoints. Each datapoint represents the accumulated
2663    /// `value` for the time period starting at the `timestamp` (in Unix epoch seconds), spanning
2664    /// one `interval`.
2665    pub values: Vec<(u32, f64)>,
2666}
2667
2668#[derive(Debug, Clone)]
2669#[non_exhaustive]
2670/// Named series of `(timestamp, value)` datapoints, each representing an instantaneous value.
2671pub struct GaugeMetric {
2672    /// Timeseries name.
2673    pub name: String,
2674    /// Unit for the instantaneous values.
2675    pub unit: MetricUnit,
2676    /// Series of `(timestamp, value)` datapoints. Each datapoint represents the `value` at the
2677    /// instant of the `timestamp` (in Unix epoch seconds).
2678    pub values: Vec<(u32, f64)>,
2679}
2680
2681#[derive(Debug, Clone)]
2682#[non_exhaustive]
2683/// Set of string labels.
2684pub struct LabelMetric {
2685    /// Label name.
2686    pub name: String,
2687    /// Label values.
2688    pub values: Vec<String>,
2689}
2690
2691#[derive(Debug, Clone)]
2692/// Individual metric in a returned metric set.
2693pub enum Metric {
2694    /// Single named value.
2695    Scalar(ScalarMetric),
2696    /// Named series of `(timestamp, value)` datapoints, each representing an accumulation over a
2697    /// specified interval.
2698    Accumulation(AccumulationMetric),
2699    /// Named series of `(timestamp, value)` datapoints, each representing an instantaneous value.
2700    Gauge(GaugeMetric),
2701    /// Set of string labels.
2702    Label(LabelMetric),
2703}
2704
2705impl From<api::metrics::Metric> for Metric {
2706    fn from(value: api::metrics::Metric) -> Self {
2707        match value {
2708            api::metrics::Metric::Scalar(sm) => Metric::Scalar(ScalarMetric {
2709                name: sm.name.into(),
2710                unit: sm.unit.into(),
2711                value: sm.value,
2712            }),
2713            api::metrics::Metric::Accumulation(am) => Metric::Accumulation(AccumulationMetric {
2714                name: am.name.into(),
2715                unit: am.unit.into(),
2716                interval: am.interval.into(),
2717                values: am.values,
2718            }),
2719            api::metrics::Metric::Gauge(gm) => Metric::Gauge(GaugeMetric {
2720                name: gm.name.into(),
2721                unit: gm.unit.into(),
2722                values: gm.values,
2723            }),
2724            api::metrics::Metric::Label(lm) => Metric::Label(LabelMetric {
2725                name: lm.name.into(),
2726                values: lm.values,
2727            }),
2728        }
2729    }
2730}
2731
2732#[derive(Debug, Clone, Default)]
2733#[non_exhaustive]
2734/// Input for [`list_streams`](crate::S2Basin::list_streams) operation.
2735pub struct ListStreamsInput {
2736    /// Filter streams whose names begin with this value.
2737    ///
2738    /// Defaults to `""`.
2739    pub prefix: StreamNamePrefix,
2740    /// Filter streams whose names are lexicographically greater than this value.
2741    ///
2742    /// Defaults to `""`.
2743    pub start_after: StreamNameStartAfter,
2744    /// Number of streams to return in a page. Will be clamped to a maximum of `1000`.
2745    ///
2746    /// Defaults to `1000`.
2747    pub limit: Option<usize>,
2748}
2749
2750impl ListStreamsInput {
2751    /// Create a new [`ListStreamsInput`] with default values.
2752    pub fn new() -> Self {
2753        Self::default()
2754    }
2755
2756    /// Set the prefix used to filter streams whose names begin with this value.
2757    pub fn with_prefix(self, prefix: StreamNamePrefix) -> Self {
2758        Self { prefix, ..self }
2759    }
2760
2761    /// Set the value used to filter streams whose names are lexicographically greater than this
2762    /// value.
2763    pub fn with_start_after(self, start_after: StreamNameStartAfter) -> Self {
2764        Self {
2765            start_after,
2766            ..self
2767        }
2768    }
2769
2770    /// Set the limit on number of streams to return in a page.
2771    pub fn with_limit(self, limit: usize) -> Self {
2772        Self {
2773            limit: Some(limit),
2774            ..self
2775        }
2776    }
2777}
2778
2779impl From<ListStreamsInput> for api::stream::ListStreamsRequest {
2780    fn from(value: ListStreamsInput) -> Self {
2781        Self {
2782            prefix: Some(value.prefix),
2783            start_after: Some(value.start_after),
2784            limit: value.limit,
2785        }
2786    }
2787}
2788
2789#[derive(Debug, Clone, Default)]
2790/// Input for [`list_all_streams`](crate::S2Basin::list_all_streams) operation.
2791pub struct ListAllStreamsInput {
2792    /// Filter streams whose names begin with this value.
2793    ///
2794    /// Defaults to `""`.
2795    pub prefix: StreamNamePrefix,
2796    /// Filter streams whose names are lexicographically greater than this value.
2797    ///
2798    /// Defaults to `""`.
2799    pub start_after: StreamNameStartAfter,
2800    /// Whether to include streams that are being deleted.
2801    ///
2802    /// Defaults to `false`.
2803    pub include_deleted: bool,
2804}
2805
2806impl ListAllStreamsInput {
2807    /// Create a new [`ListAllStreamsInput`] with default values.
2808    pub fn new() -> Self {
2809        Self::default()
2810    }
2811
2812    /// Set the prefix used to filter streams whose names begin with this value.
2813    pub fn with_prefix(self, prefix: StreamNamePrefix) -> Self {
2814        Self { prefix, ..self }
2815    }
2816
2817    /// Set the value used to filter streams whose names are lexicographically greater than this
2818    /// value.
2819    pub fn with_start_after(self, start_after: StreamNameStartAfter) -> Self {
2820        Self {
2821            start_after,
2822            ..self
2823        }
2824    }
2825
2826    /// Set whether to include streams that are being deleted.
2827    pub fn with_include_deleted(self, include_deleted: bool) -> Self {
2828        Self {
2829            include_deleted,
2830            ..self
2831        }
2832    }
2833}
2834
2835#[derive(Debug, Clone, PartialEq, Eq)]
2836#[non_exhaustive]
2837/// Stream information.
2838pub struct StreamInfo {
2839    /// Stream name.
2840    pub name: StreamName,
2841    /// Creation time.
2842    pub created_at: S2DateTime,
2843    /// Deletion time if the stream is being deleted.
2844    pub deleted_at: Option<S2DateTime>,
2845    /// Encryption algorithm for this stream, if encryption is enabled.
2846    pub cipher: Option<EncryptionAlgorithm>,
2847}
2848
2849impl TryFrom<api::stream::StreamInfo> for StreamInfo {
2850    type Error = ValidationError;
2851
2852    fn try_from(value: api::stream::StreamInfo) -> Result<Self, Self::Error> {
2853        Ok(Self {
2854            name: value.name,
2855            created_at: value.created_at.try_into()?,
2856            deleted_at: value.deleted_at.map(S2DateTime::try_from).transpose()?,
2857            cipher: value.cipher.map(Into::into),
2858        })
2859    }
2860}
2861
2862#[derive(Debug, Clone)]
2863#[non_exhaustive]
2864/// Input for [`create_stream`](crate::S2Basin::create_stream) operation.
2865pub struct CreateStreamInput {
2866    /// Stream name.
2867    pub name: StreamName,
2868    /// Configuration for the stream.
2869    ///
2870    /// See [`StreamConfig`] for defaults.
2871    pub config: Option<StreamConfig>,
2872    idempotency_token: String,
2873}
2874
2875impl CreateStreamInput {
2876    /// Create a new [`CreateStreamInput`] with the given stream name.
2877    pub fn new(name: StreamName) -> Self {
2878        Self {
2879            name,
2880            config: None,
2881            idempotency_token: idempotency_token(),
2882        }
2883    }
2884
2885    /// Set the configuration for the stream.
2886    pub fn with_config(self, config: StreamConfig) -> Self {
2887        Self {
2888            config: Some(config),
2889            ..self
2890        }
2891    }
2892}
2893
2894impl From<CreateStreamInput> for (api::stream::CreateStreamRequest, String) {
2895    fn from(value: CreateStreamInput) -> Self {
2896        (
2897            api::stream::CreateStreamRequest {
2898                stream: value.name,
2899                config: value.config.map(Into::into),
2900            },
2901            value.idempotency_token,
2902        )
2903    }
2904}
2905
2906#[derive(Debug, Clone)]
2907#[non_exhaustive]
2908/// Input for [`ensure_stream`](crate::S2Basin::ensure_stream)
2909/// operation.
2910pub struct EnsureStreamInput {
2911    /// Stream name.
2912    pub name: StreamName,
2913    /// Configuration for the stream.
2914    ///
2915    /// See [`StreamConfig`] for defaults.
2916    pub config: Option<StreamConfig>,
2917}
2918
2919impl EnsureStreamInput {
2920    /// Create a new [`EnsureStreamInput`] with the given stream name.
2921    pub fn new(name: StreamName) -> Self {
2922        Self { name, config: None }
2923    }
2924
2925    /// Set the configuration for the stream.
2926    pub fn with_config(self, config: StreamConfig) -> Self {
2927        Self {
2928            config: Some(config),
2929            ..self
2930        }
2931    }
2932}
2933
2934impl From<EnsureStreamInput> for (StreamName, Option<api::config::StreamConfig>) {
2935    fn from(value: EnsureStreamInput) -> Self {
2936        (value.name, value.config.map(Into::into))
2937    }
2938}
2939
2940#[derive(Debug, Clone)]
2941#[non_exhaustive]
2942/// Input of [`delete_stream`](crate::S2Basin::delete_stream) operation.
2943pub struct DeleteStreamInput {
2944    /// Stream name.
2945    pub name: StreamName,
2946    /// Whether to ignore `Not Found` error if the stream doesn't exist.
2947    pub ignore_not_found: bool,
2948}
2949
2950impl DeleteStreamInput {
2951    /// Create a new [`DeleteStreamInput`] with the given stream name.
2952    pub fn new(name: StreamName) -> Self {
2953        Self {
2954            name,
2955            ignore_not_found: false,
2956        }
2957    }
2958
2959    /// Set whether to ignore `Not Found` error if the stream doesn't exist.
2960    pub fn with_ignore_not_found(self, ignore_not_found: bool) -> Self {
2961        Self {
2962            ignore_not_found,
2963            ..self
2964        }
2965    }
2966}
2967
2968#[derive(Debug, Clone)]
2969#[non_exhaustive]
2970/// Input for [`reconfigure_stream`](crate::S2Basin::reconfigure_stream) operation.
2971pub struct ReconfigureStreamInput {
2972    /// Stream name.
2973    pub name: StreamName,
2974    /// Reconfiguration for [`StreamConfig`].
2975    pub config: StreamReconfiguration,
2976}
2977
2978impl ReconfigureStreamInput {
2979    /// Create a new [`ReconfigureStreamInput`] with the given stream name and reconfiguration.
2980    pub fn new(name: StreamName, config: StreamReconfiguration) -> Self {
2981        Self { name, config }
2982    }
2983}
2984
2985#[derive(Debug, Clone, PartialEq, Eq)]
2986/// Token for fencing appends to a stream.
2987///
2988/// **Note:** It must not exceed 36 bytes in length.
2989///
2990/// See [`CommandRecord::fence`] and [`AppendInput::fencing_token`].
2991pub struct FencingToken(String);
2992
2993impl FencingToken {
2994    pub(crate) fn from_server(value: String) -> Self {
2995        Self(value)
2996    }
2997
2998    /// Generate a random alphanumeric fencing token of `n` bytes.
2999    pub fn generate(n: usize) -> Result<Self, ValidationError> {
3000        rand::rng()
3001            .sample_iter(&rand::distr::Alphanumeric)
3002            .take(n)
3003            .map(char::from)
3004            .collect::<String>()
3005            .parse()
3006    }
3007}
3008
3009impl FromStr for FencingToken {
3010    type Err = ValidationError;
3011
3012    fn from_str(s: &str) -> Result<Self, Self::Err> {
3013        if s.len() > MAX_FENCING_TOKEN_LENGTH {
3014            return Err(ValidationError(format!(
3015                "fencing token exceeds {MAX_FENCING_TOKEN_LENGTH} bytes in length",
3016            )));
3017        }
3018        Ok(FencingToken(s.to_string()))
3019    }
3020}
3021
3022impl std::fmt::Display for FencingToken {
3023    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
3024        write!(f, "{}", self.0)
3025    }
3026}
3027
3028impl Deref for FencingToken {
3029    type Target = str;
3030
3031    fn deref(&self) -> &Self::Target {
3032        &self.0
3033    }
3034}
3035
3036#[derive(Debug, Clone, Copy, PartialEq)]
3037#[non_exhaustive]
3038/// A position in a stream.
3039pub struct StreamPosition {
3040    /// Sequence number assigned by the service.
3041    pub seq_num: u64,
3042    /// Timestamp. When assigned by the service, represents milliseconds since Unix epoch.
3043    /// User-specified timestamps are passed through as-is.
3044    pub timestamp: u64,
3045}
3046
3047impl StreamPosition {
3048    /// Construct a stream position.
3049    ///
3050    /// This is intended for building fixtures in downstream tests.
3051    pub fn new(seq_num: u64, timestamp: u64) -> Self {
3052        Self { seq_num, timestamp }
3053    }
3054}
3055
3056impl std::fmt::Display for StreamPosition {
3057    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
3058        write!(f, "seq_num={}, timestamp={}", self.seq_num, self.timestamp)
3059    }
3060}
3061
3062impl From<api::stream::proto::StreamPosition> for StreamPosition {
3063    fn from(value: api::stream::proto::StreamPosition) -> Self {
3064        Self {
3065            seq_num: value.seq_num,
3066            timestamp: value.timestamp,
3067        }
3068    }
3069}
3070
3071impl From<api::stream::StreamPosition> for StreamPosition {
3072    fn from(value: api::stream::StreamPosition) -> Self {
3073        Self {
3074            seq_num: value.seq_num,
3075            timestamp: value.timestamp,
3076        }
3077    }
3078}
3079
3080#[derive(Debug, Clone, PartialEq)]
3081#[non_exhaustive]
3082/// A name-value pair.
3083pub struct Header {
3084    /// Name.
3085    pub name: Bytes,
3086    /// Value.
3087    pub value: Bytes,
3088}
3089
3090impl Header {
3091    /// Create a new [`Header`] with the given name and value.
3092    pub fn new(name: impl Into<Bytes>, value: impl Into<Bytes>) -> Self {
3093        Self {
3094            name: name.into(),
3095            value: value.into(),
3096        }
3097    }
3098}
3099
3100impl From<Header> for api::stream::proto::Header {
3101    fn from(value: Header) -> Self {
3102        Self {
3103            name: value.name,
3104            value: value.value,
3105        }
3106    }
3107}
3108
3109impl From<api::stream::proto::Header> for Header {
3110    fn from(value: api::stream::proto::Header) -> Self {
3111        Self {
3112            name: value.name,
3113            value: value.value,
3114        }
3115    }
3116}
3117
3118#[derive(Debug, Clone, PartialEq)]
3119/// A record to append.
3120pub struct AppendRecord {
3121    body: Bytes,
3122    headers: Vec<Header>,
3123    timestamp: Option<u64>,
3124}
3125
3126impl AppendRecord {
3127    fn validate(self) -> Result<Self, ValidationError> {
3128        if self.metered_bytes() > RECORD_BATCH_MAX.bytes {
3129            Err(ValidationError(format!(
3130                "metered_bytes: {} exceeds {}",
3131                self.metered_bytes(),
3132                RECORD_BATCH_MAX.bytes
3133            )))
3134        } else {
3135            Ok(self)
3136        }
3137    }
3138
3139    /// Create a new [`AppendRecord`] with the given record body.
3140    pub fn new(body: impl Into<Bytes>) -> Result<Self, ValidationError> {
3141        let record = Self {
3142            body: body.into(),
3143            headers: Vec::default(),
3144            timestamp: None,
3145        };
3146        record.validate()
3147    }
3148
3149    /// Set the headers for this record.
3150    pub fn with_headers(
3151        self,
3152        headers: impl IntoIterator<Item = Header>,
3153    ) -> Result<Self, ValidationError> {
3154        let record = Self {
3155            headers: headers.into_iter().collect(),
3156            ..self
3157        };
3158        record.validate()
3159    }
3160
3161    /// Set the timestamp for this record.
3162    ///
3163    /// Precise semantics depend on [`StreamConfig::timestamping`].
3164    pub fn with_timestamp(self, timestamp: u64) -> Self {
3165        Self {
3166            timestamp: Some(timestamp),
3167            ..self
3168        }
3169    }
3170
3171    /// Get the body of this record.
3172    pub fn body(&self) -> &[u8] {
3173        &self.body
3174    }
3175
3176    /// Get the headers of this record.
3177    pub fn headers(&self) -> &[Header] {
3178        &self.headers
3179    }
3180
3181    /// Get the timestamp of this record.
3182    pub fn timestamp(&self) -> Option<u64> {
3183        self.timestamp
3184    }
3185}
3186
3187impl From<AppendRecord> for api::stream::proto::AppendRecord {
3188    fn from(value: AppendRecord) -> Self {
3189        Self {
3190            timestamp: value.timestamp,
3191            headers: value.headers.into_iter().map(Into::into).collect(),
3192            body: value.body,
3193        }
3194    }
3195}
3196
3197/// Metered byte size calculation.
3198///
3199/// Formula for a record:
3200/// ```text
3201/// 8 + 2 * len(headers) + sum(len(h.name) + len(h.value) for h in headers) + len(body)
3202/// ```
3203pub trait MeteredBytes {
3204    /// Returns the metered byte size.
3205    fn metered_bytes(&self) -> usize;
3206}
3207
3208macro_rules! metered_bytes_impl {
3209    ($ty:ty) => {
3210        impl MeteredBytes for $ty {
3211            fn metered_bytes(&self) -> usize {
3212                8 + (2 * self.headers.len())
3213                    + self
3214                        .headers
3215                        .iter()
3216                        .map(|h| h.name.len() + h.value.len())
3217                        .sum::<usize>()
3218                    + self.body.len()
3219            }
3220        }
3221    };
3222}
3223
3224metered_bytes_impl!(AppendRecord);
3225
3226impl MeteredSize for AppendRecord {
3227    fn metered_size(&self) -> usize {
3228        self.metered_bytes()
3229    }
3230}
3231
3232#[derive(Debug, Clone)]
3233/// A batch of records to append atomically.
3234///
3235/// **Note:** It must contain at least `1` record and no more than `1000`.
3236/// The total size of the batch must not exceed `1MiB` in metered bytes.
3237///
3238/// See [`AppendRecordBatches`](crate::batching::AppendRecordBatches) and
3239/// [`AppendInputs`](crate::batching::AppendInputs) for convenient and automatic batching of records
3240/// that takes care of the abovementioned constraints.
3241pub struct AppendRecordBatch(Metered<Vec<AppendRecord>>);
3242
3243impl From<Metered<Vec<AppendRecord>>> for AppendRecordBatch {
3244    fn from(records: Metered<Vec<AppendRecord>>) -> Self {
3245        Self(records)
3246    }
3247}
3248
3249impl AppendRecordBatch {
3250    /// Try to create an [`AppendRecordBatch`] from an iterator of [`AppendRecord`]s.
3251    pub fn try_from_iter<I>(iter: I) -> Result<Self, ValidationError>
3252    where
3253        I: IntoIterator<Item = AppendRecord>,
3254    {
3255        let mut records = Metered::with_capacity(RECORD_BATCH_MAX.count);
3256
3257        for record in iter {
3258            records.push(Metered::from(record));
3259
3260            if records.metered_size() > RECORD_BATCH_MAX.bytes {
3261                return Err(ValidationError(format!(
3262                    "batch size in metered bytes ({}) exceeds {}",
3263                    records.metered_size(),
3264                    RECORD_BATCH_MAX.bytes
3265                )));
3266            }
3267
3268            if records.len() > RECORD_BATCH_MAX.count {
3269                return Err(ValidationError(format!(
3270                    "number of records in the batch exceeds {}",
3271                    RECORD_BATCH_MAX.count
3272                )));
3273            }
3274        }
3275
3276        if records.is_empty() {
3277            return Err(ValidationError("batch is empty".into()));
3278        }
3279
3280        Ok(records.into())
3281    }
3282}
3283
3284impl Deref for AppendRecordBatch {
3285    type Target = [AppendRecord];
3286
3287    fn deref(&self) -> &Self::Target {
3288        &self.0[..]
3289    }
3290}
3291
3292impl MeteredBytes for AppendRecordBatch {
3293    fn metered_bytes(&self) -> usize {
3294        self.0.metered_size()
3295    }
3296}
3297
3298impl IntoIterator for AppendRecordBatch {
3299    type Item = AppendRecord;
3300    type IntoIter = std::vec::IntoIter<AppendRecord>;
3301
3302    fn into_iter(self) -> Self::IntoIter {
3303        self.0.into_iter()
3304    }
3305}
3306
3307impl<'a> IntoIterator for &'a AppendRecordBatch {
3308    type Item = &'a AppendRecord;
3309    type IntoIter = std::slice::Iter<'a, AppendRecord>;
3310
3311    fn into_iter(self) -> Self::IntoIter {
3312        self.0.iter()
3313    }
3314}
3315
3316#[derive(Debug, Clone)]
3317/// Command to signal an operation.
3318pub enum Command {
3319    /// Fence operation.
3320    Fence {
3321        /// Fencing token.
3322        fencing_token: FencingToken,
3323    },
3324    /// Trim operation.
3325    Trim {
3326        /// Trim point.
3327        trim_point: u64,
3328    },
3329}
3330
3331#[derive(Debug, Clone)]
3332#[non_exhaustive]
3333/// Command record for signaling operations to the service.
3334///
3335/// See [here](https://s2.dev/docs/rest/records/overview#command-records) for more information.
3336pub struct CommandRecord {
3337    /// Command to signal an operation.
3338    pub command: Command,
3339    /// Timestamp for this record.
3340    pub timestamp: Option<u64>,
3341}
3342
3343impl CommandRecord {
3344    const FENCE: &[u8] = b"fence";
3345    const TRIM: &[u8] = b"trim";
3346
3347    /// Create a fence command record with the given fencing token.
3348    ///
3349    /// Fencing is strongly consistent, and subsequent appends that specify a
3350    /// fencing token will fail if it does not match.
3351    pub fn fence(fencing_token: FencingToken) -> Self {
3352        Self {
3353            command: Command::Fence { fencing_token },
3354            timestamp: None,
3355        }
3356    }
3357
3358    /// Create a trim command record with the given trim point.
3359    ///
3360    /// Trim point is the desired earliest sequence number for the stream.
3361    ///
3362    /// Trimming is eventually consistent, and trimmed records may be visible
3363    /// for a brief period.
3364    pub fn trim(trim_point: u64) -> Self {
3365        Self {
3366            command: Command::Trim { trim_point },
3367            timestamp: None,
3368        }
3369    }
3370
3371    /// Set the timestamp for this record.
3372    pub fn with_timestamp(self, timestamp: u64) -> Self {
3373        Self {
3374            timestamp: Some(timestamp),
3375            ..self
3376        }
3377    }
3378}
3379
3380impl From<CommandRecord> for AppendRecord {
3381    fn from(value: CommandRecord) -> Self {
3382        let (header_value, body) = match value.command {
3383            Command::Fence { fencing_token } => (
3384                CommandRecord::FENCE,
3385                Bytes::copy_from_slice(fencing_token.as_bytes()),
3386            ),
3387            Command::Trim { trim_point } => (
3388                CommandRecord::TRIM,
3389                Bytes::copy_from_slice(&trim_point.to_be_bytes()),
3390            ),
3391        };
3392        Self {
3393            body,
3394            headers: vec![Header::new("", header_value)],
3395            timestamp: value.timestamp,
3396        }
3397    }
3398}
3399
3400#[derive(Debug, Clone)]
3401#[non_exhaustive]
3402/// Input for [`append`](crate::S2Stream::append) operation and
3403/// [`AppendSession::submit`](crate::append_session::AppendSession::submit).
3404pub struct AppendInput {
3405    /// Batch of records to append atomically.
3406    pub records: AppendRecordBatch,
3407    /// Expected sequence number for the first record in the batch.
3408    ///
3409    /// If unspecified, no matching is performed. If specified and mismatched, the append fails.
3410    pub match_seq_num: Option<u64>,
3411    /// Fencing token to match against the stream's current fencing token.
3412    ///
3413    /// If unspecified, no matching is performed. If specified and mismatched,
3414    /// the append fails. A stream defaults to `""` as its fencing token.
3415    pub fencing_token: Option<FencingToken>,
3416    /// Stream configuration to apply if the stream is created on append.
3417    ///
3418    /// Unset fields inherit the basin's default stream configuration. Ignored if the stream
3419    /// already exists.
3420    ///
3421    /// Only used by [`append`](crate::S2Stream::append). Append sessions send the header once
3422    /// at connect; see
3423    /// [`AppendSessionConfig::with_stream_config`](crate::append_session::AppendSessionConfig::with_stream_config)
3424    /// and [`ProducerConfig::with_stream_config`](crate::producer::ProducerConfig::with_stream_config).
3425    pub stream_config: Option<StreamConfig>,
3426}
3427
3428impl AppendInput {
3429    /// Create a new [`AppendInput`] with the given batch of records.
3430    pub fn new(records: AppendRecordBatch) -> Self {
3431        Self {
3432            records,
3433            match_seq_num: None,
3434            fencing_token: None,
3435            stream_config: None,
3436        }
3437    }
3438
3439    /// Set the stream configuration to apply if the stream is created on append.
3440    pub fn with_stream_config(self, stream_config: StreamConfig) -> Self {
3441        Self {
3442            stream_config: Some(stream_config),
3443            ..self
3444        }
3445    }
3446
3447    /// Set the expected sequence number for the first record in the batch.
3448    pub fn with_match_seq_num(self, match_seq_num: u64) -> Self {
3449        Self {
3450            match_seq_num: Some(match_seq_num),
3451            ..self
3452        }
3453    }
3454
3455    /// Set the fencing token to match against the stream's current fencing token.
3456    pub fn with_fencing_token(self, fencing_token: FencingToken) -> Self {
3457        Self {
3458            fencing_token: Some(fencing_token),
3459            ..self
3460        }
3461    }
3462}
3463
3464impl From<AppendInput> for api::stream::proto::AppendInput {
3465    fn from(value: AppendInput) -> Self {
3466        Self {
3467            records: value.records.iter().cloned().map(Into::into).collect(),
3468            match_seq_num: value.match_seq_num,
3469            fencing_token: value.fencing_token.map(|t| t.to_string()),
3470        }
3471    }
3472}
3473
3474#[derive(Debug, Clone, PartialEq)]
3475#[non_exhaustive]
3476/// Acknowledgement for an [`AppendInput`].
3477pub struct AppendAck {
3478    /// Sequence number and timestamp of the first record that was appended.
3479    pub start: StreamPosition,
3480    /// Sequence number of the last record that was appended + 1, and timestamp of the last record
3481    /// that was appended.
3482    ///
3483    /// The difference between `end.seq_num` and `start.seq_num` will be the number of records
3484    /// appended.
3485    pub end: StreamPosition,
3486    /// Sequence number that will be assigned to the next record on the stream, and timestamp of
3487    /// the last record on the stream.
3488    ///
3489    /// This can be greater than the `end` position in case of concurrent appends.
3490    pub tail: StreamPosition,
3491}
3492
3493impl AppendAck {
3494    /// Construct an append acknowledgement.
3495    ///
3496    /// This is intended for building fixtures in downstream tests.
3497    pub fn new(start: StreamPosition, end: StreamPosition, tail: StreamPosition) -> Self {
3498        Self { start, end, tail }
3499    }
3500}
3501
3502impl From<api::stream::proto::AppendAck> for AppendAck {
3503    fn from(value: api::stream::proto::AppendAck) -> Self {
3504        Self {
3505            start: value.start.unwrap_or_default().into(),
3506            end: value.end.unwrap_or_default().into(),
3507            tail: value.tail.unwrap_or_default().into(),
3508        }
3509    }
3510}
3511
3512#[derive(Debug, Clone, Copy)]
3513/// Starting position for reading from a stream.
3514pub enum ReadFrom {
3515    /// Read from this sequence number.
3516    SeqNum(u64),
3517    /// Read from this timestamp.
3518    Timestamp(u64),
3519    /// Read from N records before the tail.
3520    TailOffset(u64),
3521}
3522
3523impl Default for ReadFrom {
3524    fn default() -> Self {
3525        Self::SeqNum(0)
3526    }
3527}
3528
3529#[derive(Debug, Default, Clone)]
3530#[non_exhaustive]
3531/// Where to start reading.
3532pub struct ReadStart {
3533    /// Starting position.
3534    ///
3535    /// Defaults to reading from sequence number `0`.
3536    pub from: ReadFrom,
3537    /// Whether to start from tail if the requested starting position is beyond it.
3538    ///
3539    /// Defaults to `false` (errors if position is beyond tail).
3540    pub clamp_to_tail: bool,
3541}
3542
3543impl ReadStart {
3544    /// Create a new [`ReadStart`] with default values.
3545    pub fn new() -> Self {
3546        Self::default()
3547    }
3548
3549    /// Set the starting position.
3550    pub fn with_from(self, from: ReadFrom) -> Self {
3551        Self { from, ..self }
3552    }
3553
3554    /// Set whether to start from tail if the requested starting position is beyond it.
3555    pub fn with_clamp_to_tail(self, clamp_to_tail: bool) -> Self {
3556        Self {
3557            clamp_to_tail,
3558            ..self
3559        }
3560    }
3561}
3562
3563impl From<ReadStart> for api::stream::ReadStart {
3564    fn from(value: ReadStart) -> Self {
3565        let (seq_num, timestamp, tail_offset) = match value.from {
3566            ReadFrom::SeqNum(n) => (Some(n), None, None),
3567            ReadFrom::Timestamp(t) => (None, Some(t), None),
3568            ReadFrom::TailOffset(o) => (None, None, Some(o)),
3569        };
3570        Self {
3571            seq_num,
3572            timestamp,
3573            tail_offset,
3574            clamp: if value.clamp_to_tail {
3575                Some(true)
3576            } else {
3577                None
3578            },
3579        }
3580    }
3581}
3582
3583#[derive(Debug, Clone, Default)]
3584#[non_exhaustive]
3585/// Limits on how much to read.
3586pub struct ReadLimits {
3587    /// Limit on number of records.
3588    ///
3589    /// Defaults to `1000` for non-streaming read.
3590    pub count: Option<usize>,
3591    /// Limit on total metered bytes of records.
3592    ///
3593    /// Defaults to `1MiB` for non-streaming read.
3594    pub bytes: Option<usize>,
3595}
3596
3597impl ReadLimits {
3598    /// Create a new [`ReadLimits`] with default values.
3599    pub fn new() -> Self {
3600        Self::default()
3601    }
3602
3603    /// Set the limit on number of records.
3604    pub fn with_count(self, count: usize) -> Self {
3605        Self {
3606            count: Some(count),
3607            ..self
3608        }
3609    }
3610
3611    /// Set the limit on total metered bytes of records.
3612    pub fn with_bytes(self, bytes: usize) -> Self {
3613        Self {
3614            bytes: Some(bytes),
3615            ..self
3616        }
3617    }
3618}
3619
3620#[derive(Debug, Clone, Default)]
3621#[non_exhaustive]
3622/// When to stop reading.
3623pub struct ReadStop {
3624    /// Limits on how much to read.
3625    ///
3626    /// See [`ReadLimits`] for defaults.
3627    pub limits: ReadLimits,
3628    /// Timestamp at which to stop (exclusive).
3629    ///
3630    /// Defaults to `None`.
3631    pub until: Option<RangeTo<u64>>,
3632    /// Duration in seconds to wait for new records before stopping. Will be clamped to `60`
3633    /// seconds for [`read`](crate::S2Stream::read).
3634    ///
3635    /// Defaults to:
3636    /// - `0` (no wait) for [`read`](crate::S2Stream::read).
3637    /// - `0` (no wait) for [`read_session`](crate::S2Stream::read_session) if `limits` or `until`
3638    ///   is specified.
3639    /// - Infinite wait for [`read_session`](crate::S2Stream::read_session) if neither `limits` nor
3640    ///   `until` is specified.
3641    pub wait: Option<u32>,
3642}
3643
3644impl ReadStop {
3645    /// Create a new [`ReadStop`] with default values.
3646    pub fn new() -> Self {
3647        Self::default()
3648    }
3649
3650    /// Set the limits on how much to read.
3651    pub fn with_limits(self, limits: ReadLimits) -> Self {
3652        Self { limits, ..self }
3653    }
3654
3655    /// Set the timestamp at which to stop (exclusive).
3656    pub fn with_until(self, until: RangeTo<u64>) -> Self {
3657        Self {
3658            until: Some(until),
3659            ..self
3660        }
3661    }
3662
3663    /// Set the duration in seconds to wait for new records before stopping.
3664    pub fn with_wait(self, wait: u32) -> Self {
3665        Self {
3666            wait: Some(wait),
3667            ..self
3668        }
3669    }
3670}
3671
3672impl From<ReadStop> for api::stream::ReadEnd {
3673    fn from(value: ReadStop) -> Self {
3674        Self {
3675            count: value.limits.count,
3676            bytes: value.limits.bytes,
3677            until: value.until.map(|r| r.end),
3678            wait: value.wait,
3679        }
3680    }
3681}
3682
3683#[derive(Debug, Clone, Default)]
3684#[non_exhaustive]
3685/// Input for [`read`](crate::S2Stream::read) and [`read_session`](crate::S2Stream::read_session)
3686/// operations.
3687pub struct ReadInput {
3688    /// Where to start reading.
3689    ///
3690    /// See [`ReadStart`] for defaults.
3691    pub start: ReadStart,
3692    /// When to stop reading.
3693    ///
3694    /// See [`ReadStop`] for defaults.
3695    pub stop: ReadStop,
3696    /// Whether to filter out command records from the stream when reading.
3697    ///
3698    /// Defaults to `false`.
3699    pub ignore_command_records: bool,
3700    /// Stream configuration to apply if the stream is created on read.
3701    ///
3702    /// Unset fields inherit the basin's default stream configuration. Ignored if the stream
3703    /// already exists.
3704    pub stream_config: Option<StreamConfig>,
3705}
3706
3707#[derive(Debug, Clone, Copy, Default, Eq, PartialEq)]
3708#[non_exhaustive]
3709/// Retry policy for a continuous read session.
3710pub enum ReadSessionRetryPolicy {
3711    /// Stop after the retry budget configured by [`RetryConfig`] is exhausted.
3712    #[default]
3713    Budgeted,
3714    /// Keep retrying retryable failures after the configured retry budget is exhausted.
3715    ///
3716    /// This also applies while establishing the initial session, so
3717    /// [`read_session`](crate::S2Stream::read_session) may remain pending through retryable
3718    /// failures until it connects or the future is cancelled.
3719    Indefinite,
3720}
3721
3722#[derive(Debug, Clone, Default)]
3723#[non_exhaustive]
3724/// Configuration for a continuous read session.
3725pub struct ReadSessionConfig {
3726    /// Policy for retrying retryable failures.
3727    ///
3728    /// Clean stream ends and non-retryable failures always terminate the session.
3729    ///
3730    /// Defaults to [`ReadSessionRetryPolicy::Budgeted`].
3731    pub retry_policy: ReadSessionRetryPolicy,
3732}
3733
3734impl ReadSessionConfig {
3735    /// Create a new [`ReadSessionConfig`] with default settings.
3736    pub fn new() -> Self {
3737        Self::default()
3738    }
3739
3740    /// Set the policy for retrying retryable failures.
3741    pub fn with_retry_policy(self, retry_policy: ReadSessionRetryPolicy) -> Self {
3742        Self {
3743            retry_policy,
3744            ..self
3745        }
3746    }
3747}
3748
3749impl ReadInput {
3750    /// Create a new [`ReadInput`] with default values.
3751    pub fn new() -> Self {
3752        Self::default()
3753    }
3754
3755    /// Set where to start reading.
3756    pub fn with_start(self, start: ReadStart) -> Self {
3757        Self { start, ..self }
3758    }
3759
3760    /// Set when to stop reading.
3761    pub fn with_stop(self, stop: ReadStop) -> Self {
3762        Self { stop, ..self }
3763    }
3764
3765    /// Set whether to filter out command records from the stream when reading.
3766    pub fn with_ignore_command_records(self, ignore_command_records: bool) -> Self {
3767        Self {
3768            ignore_command_records,
3769            ..self
3770        }
3771    }
3772
3773    /// Set the stream configuration to apply if the stream is created on read.
3774    pub fn with_stream_config(self, stream_config: StreamConfig) -> Self {
3775        Self {
3776            stream_config: Some(stream_config),
3777            ..self
3778        }
3779    }
3780}
3781
3782#[derive(Debug, Clone)]
3783#[non_exhaustive]
3784/// Record that is durably sequenced on a stream.
3785pub struct SequencedRecord {
3786    /// Sequence number assigned to this record.
3787    pub seq_num: u64,
3788    /// Body of this record.
3789    pub body: Bytes,
3790    /// Headers for this record.
3791    pub headers: Vec<Header>,
3792    /// Timestamp for this record.
3793    pub timestamp: u64,
3794}
3795
3796impl SequencedRecord {
3797    /// Construct a sequenced record from its plain-data fields.
3798    ///
3799    /// This is intended for building fixtures in downstream tests.
3800    pub fn from_parts(
3801        seq_num: u64,
3802        timestamp: u64,
3803        headers: Vec<Header>,
3804        body: impl Into<Bytes>,
3805    ) -> Self {
3806        Self {
3807            seq_num,
3808            timestamp,
3809            body: body.into(),
3810            headers,
3811        }
3812    }
3813
3814    /// Whether this is a command record.
3815    pub fn is_command_record(&self) -> bool {
3816        self.headers.len() == 1 && *self.headers[0].name == *b""
3817    }
3818}
3819
3820impl From<api::stream::proto::SequencedRecord> for SequencedRecord {
3821    fn from(value: api::stream::proto::SequencedRecord) -> Self {
3822        Self {
3823            seq_num: value.seq_num,
3824            body: value.body,
3825            headers: value.headers.into_iter().map(Into::into).collect(),
3826            timestamp: value.timestamp,
3827        }
3828    }
3829}
3830
3831metered_bytes_impl!(SequencedRecord);
3832
3833#[derive(Debug, Clone)]
3834#[non_exhaustive]
3835/// Batch of records returned by [`read`](crate::S2Stream::read) or streamed by
3836/// [`read_session`](crate::S2Stream::read_session).
3837pub struct ReadBatch {
3838    /// Records that are durably sequenced on the stream.
3839    ///
3840    /// It can be empty only for a [`read`](crate::S2Stream::read) operation when:
3841    /// - the [`stop condition`](ReadInput::stop) was already met, or
3842    /// - all records in the batch were command records and
3843    ///   [`ignore_command_records`](ReadInput::ignore_command_records) was set to `true`.
3844    pub records: Vec<SequencedRecord>,
3845    /// Sequence number that will be assigned to the next record on the stream, and timestamp of
3846    /// the last record.
3847    ///
3848    /// It will only be present when reading recent records.
3849    pub tail: Option<StreamPosition>,
3850}
3851
3852impl ReadBatch {
3853    /// Construct a read batch.
3854    ///
3855    /// This is intended for building fixtures in downstream tests.
3856    pub fn new(records: Vec<SequencedRecord>, tail: Option<StreamPosition>) -> Self {
3857        Self { records, tail }
3858    }
3859
3860    pub(crate) fn from_api(batch: api::stream::proto::ReadBatch) -> Self {
3861        Self {
3862            records: batch.records.into_iter().map(Into::into).collect(),
3863            tail: batch.tail.map(Into::into),
3864        }
3865    }
3866}
3867
3868/// A stream of values of type `Result<T, RequestError>`.
3869pub type Streaming<T> = Pin<Box<dyn Send + futures_core::Stream<Item = Result<T, RequestError>>>>;
3870
3871fn idempotency_token() -> String {
3872    uuid::Uuid::new_v4().simple().to_string()
3873}
3874
3875#[cfg(test)]
3876mod tests {
3877    use proptest::prelude::*;
3878    use rstest::rstest;
3879
3880    use super::*;
3881
3882    type HeaderParts = (Vec<u8>, Vec<u8>);
3883    type AppendRecordParts = (Vec<u8>, Vec<HeaderParts>);
3884
3885    fn byte_vec_strategy(max_len: usize) -> impl Strategy<Value = Vec<u8>> {
3886        prop::collection::vec(any::<u8>(), 0..=max_len)
3887    }
3888
3889    fn header_parts_strategy() -> impl Strategy<Value = HeaderParts> {
3890        (byte_vec_strategy(32), byte_vec_strategy(64))
3891    }
3892
3893    fn string_strategy(max_chars: usize) -> impl Strategy<Value = String> {
3894        prop::collection::vec(any::<char>(), 0..=max_chars)
3895            .prop_map(|chars| chars.into_iter().collect())
3896    }
3897
3898    fn read_from_strategy() -> impl Strategy<Value = ReadFrom> {
3899        prop_oneof![
3900            any::<u64>().prop_map(ReadFrom::SeqNum),
3901            any::<u64>().prop_map(ReadFrom::Timestamp),
3902            any::<u64>().prop_map(ReadFrom::TailOffset),
3903        ]
3904    }
3905
3906    fn append_record_parts_strategy() -> impl Strategy<Value = AppendRecordParts> {
3907        (
3908            byte_vec_strategy(256),
3909            prop::collection::vec(header_parts_strategy(), 0..=16),
3910        )
3911    }
3912
3913    fn proto_stream_position_strategy() -> impl Strategy<Value = api::stream::proto::StreamPosition>
3914    {
3915        (any::<u64>(), any::<u64>()).prop_map(|(seq_num, timestamp)| {
3916            api::stream::proto::StreamPosition { seq_num, timestamp }
3917        })
3918    }
3919
3920    fn headers_from_parts(headers: &[HeaderParts]) -> Vec<Header> {
3921        headers
3922            .iter()
3923            .map(|(name, value)| Header::new(Bytes::from(name.clone()), Bytes::from(value.clone())))
3924            .collect()
3925    }
3926
3927    fn expected_metered_bytes(body: &[u8], headers: &[HeaderParts]) -> usize {
3928        8 + (2 * headers.len())
3929            + headers
3930                .iter()
3931                .map(|(name, value)| name.len() + value.len())
3932                .sum::<usize>()
3933            + body.len()
3934    }
3935
3936    // -- S2DateTime --
3937
3938    #[test]
3939    fn s2_datetime_parse_valid_rfc3339() {
3940        let dt: S2DateTime = "2024-01-15T12:30:00Z".parse().unwrap();
3941        assert_eq!(dt.to_string(), "2024-01-15T12:30:00Z");
3942    }
3943
3944    #[test]
3945    fn s2_datetime_parse_with_offset() {
3946        let dt: S2DateTime = "2024-06-01T08:00:00+05:30".parse().unwrap();
3947        assert_eq!(dt.to_string(), "2024-06-01T08:00:00+05:30");
3948
3949        let offset_dt: time::OffsetDateTime = dt.into();
3950        assert_eq!(
3951            offset_dt.offset(),
3952            time::UtcOffset::from_hms(5, 30, 0).unwrap()
3953        );
3954    }
3955
3956    #[test]
3957    fn s2_datetime_parse_invalid() {
3958        let err = "not-a-date".parse::<S2DateTime>();
3959        assert!(err.is_err());
3960    }
3961
3962    #[test]
3963    fn s2_datetime_roundtrip_via_offset_datetime() {
3964        let odt = time::OffsetDateTime::from_unix_timestamp(1_700_000_000).unwrap();
3965        let dt = S2DateTime::try_from(odt).unwrap();
3966        let back: time::OffsetDateTime = dt.into();
3967        assert_eq!(odt, back);
3968    }
3969
3970    // -- AccountEndpoint --
3971
3972    #[rstest]
3973    #[case::https_with_scheme("https://aws.s2.dev", Scheme::HTTPS)]
3974    #[case::http_with_scheme("http://localhost:8080", Scheme::HTTP)]
3975    #[case::default_https("aws.s2.dev", Scheme::HTTPS)]
3976    fn account_endpoint_parse(#[case] input: &str, #[case] expected_scheme: Scheme) {
3977        let ep: AccountEndpoint = input.parse().unwrap();
3978        assert_eq!(ep.scheme, expected_scheme);
3979    }
3980
3981    // -- BasinEndpoint --
3982
3983    #[rstest]
3984    #[case::https_parent_zone("https://{basin}.b.s2.dev", Scheme::HTTPS, true)]
3985    #[case::http_direct("http://localhost:8080", Scheme::HTTP, false)]
3986    #[case::default_https_parent_zone("{basin}.b.s2.dev", Scheme::HTTPS, true)]
3987    fn basin_endpoint_parse(
3988        #[case] input: &str,
3989        #[case] expected_scheme: Scheme,
3990        #[case] expected_parent_zone: bool,
3991    ) {
3992        let ep: BasinEndpoint = input.parse().unwrap();
3993        assert_eq!(ep.scheme, expected_scheme);
3994        assert_eq!(
3995            matches!(ep.authority, BasinAuthority::ParentZone(_)),
3996            expected_parent_zone
3997        );
3998    }
3999
4000    // -- S2Endpoints --
4001
4002    #[test]
4003    fn s2_endpoints_new_requires_same_scheme() {
4004        let account: AccountEndpoint = "https://aws.s2.dev".parse().unwrap();
4005        let basin: BasinEndpoint = "http://localhost:8080".parse().unwrap();
4006        let err = S2Endpoints::new(account, basin);
4007        assert!(err.is_err());
4008    }
4009
4010    #[test]
4011    fn s2_endpoints_new_same_scheme_succeeds() {
4012        let account: AccountEndpoint = "https://aws.s2.dev".parse().unwrap();
4013        let basin: BasinEndpoint = "https://{basin}.b.s2.dev".parse().unwrap();
4014        let ep = S2Endpoints::new(account, basin).unwrap();
4015        assert_eq!(ep.scheme, Scheme::HTTPS);
4016    }
4017
4018    #[test]
4019    fn s2_endpoints_for_endpoint_defaults_to_https() {
4020        let ep = S2Endpoints::for_endpoint("localhost:8080").unwrap();
4021        let authority: Authority = "localhost:8080".parse().unwrap();
4022        assert_eq!(ep.scheme, Scheme::HTTPS);
4023        assert_eq!(ep.account_authority, authority);
4024        assert_eq!(ep.basin_authority, BasinAuthority::Direct(authority));
4025    }
4026
4027    #[test]
4028    fn s2_endpoints_for_endpoint_accepts_explicit_scheme() {
4029        let ep = S2Endpoints::for_endpoint("http://localhost:8080").unwrap();
4030        assert_eq!(ep.scheme, Scheme::HTTP);
4031    }
4032
4033    #[test]
4034    fn s2_endpoints_for_endpoint_rejects_invalid_endpoint() {
4035        assert!(S2Endpoints::for_endpoint("not a valid endpoint").is_err());
4036    }
4037
4038    // -- Compression --
4039
4040    #[rstest]
4041    #[case::none(Compression::None, CompressionAlgorithm::None)]
4042    #[case::gzip(Compression::Gzip, CompressionAlgorithm::Gzip)]
4043    #[case::zstd(Compression::Zstd, CompressionAlgorithm::Zstd)]
4044    fn compression_conversion(#[case] sdk: Compression, #[case] api: CompressionAlgorithm) {
4045        assert_eq!(CompressionAlgorithm::from(sdk), api);
4046    }
4047
4048    // -- RetryConfig --
4049
4050    #[test]
4051    fn retry_config_defaults() {
4052        let rc = RetryConfig::default();
4053        assert_eq!(rc.max_attempts.get(), 3);
4054        assert_eq!(rc.min_base_delay, Duration::from_millis(100));
4055        assert_eq!(rc.max_base_delay, Duration::from_secs(1));
4056        assert!(matches!(rc.append_retry_policy, AppendRetryPolicy::All));
4057    }
4058
4059    #[test]
4060    fn retry_config_max_retries() {
4061        let rc = RetryConfig::default();
4062        assert_eq!(rc.max_retries(), 2);
4063    }
4064
4065    // -- S2Config --
4066
4067    #[test]
4068    fn s2_config_defaults() {
4069        let cfg = S2Config::new("test-token");
4070        assert_eq!(cfg.connection_timeout, Duration::from_secs(3));
4071        assert_eq!(cfg.request_timeout, Duration::from_secs(5));
4072        assert!(!cfg.insecure_skip_cert_verification);
4073    }
4074
4075    #[cfg(feature = "_hidden")]
4076    #[rstest]
4077    #[case::matching_compression("content-encoding", "gzip", Compression::Gzip)]
4078    #[case::mixed_case("Content-Encoding", "identity", Compression::None)]
4079    #[case::empty_value("content-encoding", "", Compression::None)]
4080    fn default_headers_reject_content_encoding(
4081        #[case] name: &str,
4082        #[case] value: &str,
4083        #[case] compression: Compression,
4084    ) {
4085        let headers = HeaderMap::from_iter([(
4086            name.parse::<http::header::HeaderName>().unwrap(),
4087            HeaderValue::from_str(value).unwrap(),
4088        )]);
4089        let error = S2Config::new("token")
4090            .with_compression(compression)
4091            .with_default_headers(headers)
4092            .unwrap_err();
4093        assert!(error.0.contains("Content-Encoding"));
4094        assert!(error.0.contains("with_compression"));
4095    }
4096
4097    #[cfg(feature = "_hidden")]
4098    #[rstest]
4099    #[case::content_type_s2s("content-type", "s2s/proto")]
4100    #[case::content_type_protobuf("content-type", "application/protobuf")]
4101    #[case::content_type_json("content-type", "application/json")]
4102    #[case::content_type_mixed_case("Content-Type", "s2s/proto")]
4103    #[case::content_type_empty("content-type", "")]
4104    #[case::content_length("content-length", "123")]
4105    #[case::content_length_mixed_case("Content-Length", "0")]
4106    #[case::content_length_empty("content-length", "")]
4107    #[case::transfer_encoding("transfer-encoding", "chunked")]
4108    #[case::transfer_encoding_mixed_case("Transfer-Encoding", "chunked")]
4109    #[case::transfer_encoding_empty("transfer-encoding", "")]
4110    fn default_headers_reject_framing_headers(#[case] name: &str, #[case] value: &str) {
4111        let headers = HeaderMap::from_iter([(
4112            name.parse::<http::header::HeaderName>().unwrap(),
4113            HeaderValue::from_str(value).unwrap(),
4114        )]);
4115        let error = S2Config::new("token")
4116            .with_default_headers(headers)
4117            .unwrap_err();
4118        assert!(error.0.contains(&name.to_ascii_lowercase()));
4119        assert!(error.0.contains("framing"));
4120    }
4121
4122    // -- StorageClass --
4123
4124    #[rstest]
4125    #[case::standard(StorageClass::Standard)]
4126    #[case::express(StorageClass::Express)]
4127    fn storage_class_roundtrip(#[case] sdk: StorageClass) {
4128        let api: api::config::StorageClass = sdk.into();
4129        let back: StorageClass = api.into();
4130        assert_eq!(back, sdk);
4131    }
4132
4133    // -- RetentionPolicy --
4134
4135    #[rstest]
4136    #[case::age(RetentionPolicy::Age(3600))]
4137    #[case::infinite(RetentionPolicy::Infinite)]
4138    fn retention_policy_roundtrip(#[case] sdk: RetentionPolicy) {
4139        let api: api::config::RetentionPolicy = sdk.into();
4140        let back: RetentionPolicy = api.into();
4141        assert_eq!(back, sdk);
4142    }
4143
4144    // -- TimestampingMode --
4145
4146    #[rstest]
4147    #[case::client_prefer(
4148        TimestampingMode::ClientPrefer,
4149        api::config::TimestampingMode::ClientPrefer
4150    )]
4151    #[case::client_require(
4152        TimestampingMode::ClientRequire,
4153        api::config::TimestampingMode::ClientRequire
4154    )]
4155    #[case::arrival(TimestampingMode::Arrival, api::config::TimestampingMode::Arrival)]
4156    fn timestamping_mode_roundtrip(
4157        #[case] sdk: TimestampingMode,
4158        #[case] expected_api: api::config::TimestampingMode,
4159    ) {
4160        let converted: api::config::TimestampingMode = sdk.into();
4161        assert_eq!(converted, expected_api);
4162        let back: TimestampingMode = converted.into();
4163        assert_eq!(back, sdk);
4164    }
4165
4166    // -- TimestampingConfig --
4167
4168    #[test]
4169    fn timestamping_config_roundtrip() {
4170        let sdk = TimestampingConfig {
4171            mode: Some(TimestampingMode::Arrival),
4172            uncapped: Some(true),
4173        };
4174        let api: api::config::TimestampingConfig = sdk.into();
4175        let back: TimestampingConfig = api.into();
4176        assert_eq!(back, sdk);
4177    }
4178
4179    // -- DeleteOnEmptyConfig --
4180
4181    #[test]
4182    fn delete_on_empty_config_roundtrip() {
4183        let sdk = DeleteOnEmptyConfig::new().with_min_age(Duration::from_secs(300));
4184        let api: api::config::DeleteOnEmptyConfig = sdk.into();
4185        let back: DeleteOnEmptyConfig = api.into();
4186        assert_eq!(back, sdk);
4187    }
4188
4189    // -- StreamConfig --
4190
4191    #[test]
4192    fn stream_config_builder_and_roundtrip() {
4193        let sdk = StreamConfig::new()
4194            .with_storage_class(StorageClass::Express)
4195            .with_retention_policy(RetentionPolicy::Age(86400))
4196            .with_timestamping(TimestampingConfig {
4197                mode: Some(TimestampingMode::ClientPrefer),
4198                uncapped: None,
4199            })
4200            .with_delete_on_empty(DeleteOnEmptyConfig { min_age_secs: 60 });
4201        let api: api::config::StreamConfig = sdk.clone().into();
4202        let back: StreamConfig = api.into();
4203        assert_eq!(back, sdk);
4204    }
4205
4206    // -- BasinConfig --
4207
4208    #[test]
4209    fn basin_config_builder_and_roundtrip() {
4210        let sdk = BasinConfig::new()
4211            .with_default_stream_config(
4212                StreamConfig::new().with_storage_class(StorageClass::Standard),
4213            )
4214            .with_create_stream_on_append(true)
4215            .with_create_stream_on_read(false);
4216        let api: api::config::BasinConfig = sdk.clone().into();
4217        let back: BasinConfig = api.into();
4218        assert_eq!(back, sdk);
4219    }
4220
4221    // -- FencingToken --
4222
4223    proptest! {
4224        #[test]
4225        fn fencing_token_parse_accepts_only_within_byte_limit(
4226            token in string_strategy(MAX_FENCING_TOKEN_LENGTH + 8),
4227        ) {
4228            let parsed = token.parse::<FencingToken>();
4229
4230            if token.len() <= MAX_FENCING_TOKEN_LENGTH {
4231                prop_assert_eq!(parsed.unwrap().to_string(), token);
4232            } else {
4233                prop_assert!(parsed.is_err());
4234            }
4235        }
4236    }
4237
4238    // -- StreamPosition --
4239
4240    #[test]
4241    fn stream_position_display() {
4242        let pos = StreamPosition {
4243            seq_num: 42,
4244            timestamp: 1700000000,
4245        };
4246        assert_eq!(pos.to_string(), "seq_num=42, timestamp=1700000000");
4247    }
4248
4249    proptest! {
4250        #[test]
4251        fn stream_position_conversions_preserve_values(seq_num in any::<u64>(), timestamp in any::<u64>()) {
4252            let proto: StreamPosition = api::stream::proto::StreamPosition {
4253                seq_num,
4254                timestamp,
4255            }
4256            .into();
4257            prop_assert_eq!(proto.seq_num, seq_num);
4258            prop_assert_eq!(proto.timestamp, timestamp);
4259
4260            let api: StreamPosition = api::stream::StreamPosition {
4261                seq_num,
4262                timestamp,
4263            }
4264            .into();
4265            prop_assert_eq!(api.seq_num, seq_num);
4266            prop_assert_eq!(api.timestamp, timestamp);
4267        }
4268    }
4269
4270    // -- Header --
4271
4272    proptest! {
4273        #[test]
4274        fn header_proto_roundtrip_preserves_binary_parts(
4275            name in byte_vec_strategy(64),
4276            value in byte_vec_strategy(128),
4277        ) {
4278            let header = Header::new(Bytes::from(name.clone()), Bytes::from(value.clone()));
4279            let proto: api::stream::proto::Header = header.into();
4280            let back: Header = proto.into();
4281
4282            prop_assert_eq!(back.name.as_ref(), name.as_slice());
4283            prop_assert_eq!(back.value.as_ref(), value.as_slice());
4284        }
4285    }
4286
4287    // -- AppendRecord --
4288
4289    #[test]
4290    fn append_record_too_large() {
4291        let big_body = vec![0u8; RECORD_BATCH_MAX.bytes + 1];
4292        assert!(AppendRecord::new(big_body).is_err());
4293    }
4294
4295    // -- MeteredBytes --
4296
4297    proptest! {
4298        #[test]
4299        fn append_record_preserves_fields_and_metered_byte_formula(
4300            (body, headers) in append_record_parts_strategy(),
4301            timestamp in proptest::option::of(any::<u64>()),
4302        ) {
4303            let mut record = AppendRecord::new(body.clone())
4304                .unwrap()
4305                .with_headers(headers_from_parts(&headers))
4306                .unwrap();
4307            if let Some(timestamp) = timestamp {
4308                record = record.with_timestamp(timestamp);
4309            }
4310
4311            prop_assert_eq!(record.body(), body.as_slice());
4312            prop_assert_eq!(record.headers().len(), headers.len());
4313            prop_assert_eq!(record.timestamp(), timestamp);
4314            prop_assert_eq!(record.metered_bytes(), expected_metered_bytes(&body, &headers));
4315
4316            for (actual, (expected_name, expected_value)) in record.headers().iter().zip(headers.iter()) {
4317                prop_assert_eq!(actual.name.as_ref(), expected_name.as_slice());
4318                prop_assert_eq!(actual.value.as_ref(), expected_value.as_slice());
4319            }
4320        }
4321    }
4322
4323    // -- AppendRecordBatch --
4324
4325    #[test]
4326    fn append_record_batch_empty_is_err() {
4327        let result = AppendRecordBatch::try_from_iter(vec![]);
4328        assert!(result.is_err());
4329    }
4330
4331    #[test]
4332    fn append_record_batch_too_many_records() {
4333        let records: Vec<_> = (0..1001).map(|_| AppendRecord::new("x").unwrap()).collect();
4334        let result = AppendRecordBatch::try_from_iter(records);
4335        assert!(result.is_err());
4336    }
4337
4338    proptest! {
4339        #[test]
4340        fn append_record_batch_metered_bytes_is_sum_of_records(
4341            records in prop::collection::vec(append_record_parts_strategy(), 1..=32),
4342        ) {
4343            let expected = records
4344                .iter()
4345                .map(|(body, headers)| expected_metered_bytes(body, headers))
4346                .sum::<usize>();
4347            let records = records
4348                .into_iter()
4349                .map(|(body, headers)| {
4350                    AppendRecord::new(body)
4351                        .unwrap()
4352                        .with_headers(headers_from_parts(&headers))
4353                        .unwrap()
4354                })
4355                .collect::<Vec<_>>();
4356
4357            let batch = AppendRecordBatch::try_from_iter(records).unwrap();
4358            prop_assert_eq!(batch.metered_bytes(), expected);
4359            prop_assert_eq!(batch.iter().map(MeteredBytes::metered_bytes).sum::<usize>(), expected);
4360        }
4361    }
4362
4363    // -- CommandRecord --
4364
4365    #[test]
4366    fn command_record_fence() {
4367        let token: FencingToken = "tok".parse().unwrap();
4368        let cmd = CommandRecord::fence(token);
4369        let record: AppendRecord = cmd.into();
4370        assert_eq!(record.headers().len(), 1);
4371        assert_eq!(record.headers()[0].name.as_ref(), b"");
4372        assert_eq!(record.headers()[0].value.as_ref(), b"fence");
4373        assert_eq!(record.body(), b"tok");
4374    }
4375
4376    #[test]
4377    fn command_record_trim() {
4378        let cmd = CommandRecord::trim(42);
4379        let record: AppendRecord = cmd.into();
4380        assert_eq!(record.headers().len(), 1);
4381        assert_eq!(record.headers()[0].value.as_ref(), b"trim");
4382        assert_eq!(record.body(), &42u64.to_be_bytes());
4383    }
4384
4385    // -- SequencedRecord --
4386
4387    #[rstest]
4388    #[case::command(vec![Header::new("", "fence")], true)]
4389    #[case::regular(vec![Header::new("key", "value")], false)]
4390    #[case::no_headers(vec![], false)]
4391    fn sequenced_record_command_detection(#[case] headers: Vec<Header>, #[case] expected: bool) {
4392        let record = SequencedRecord {
4393            seq_num: 0,
4394            body: Bytes::from("data"),
4395            headers,
4396            timestamp: 0,
4397        };
4398        assert_eq!(record.is_command_record(), expected);
4399    }
4400
4401    // -- ReadStart --
4402
4403    proptest! {
4404        #[test]
4405        fn read_start_to_api_sets_only_selected_position_field(
4406            from in read_from_strategy(),
4407            clamp_to_tail in any::<bool>(),
4408        ) {
4409            let (seq_num, timestamp, tail_offset) = match from {
4410                ReadFrom::SeqNum(value) => (Some(value), None, None),
4411                ReadFrom::Timestamp(value) => (None, Some(value), None),
4412                ReadFrom::TailOffset(value) => (None, None, Some(value)),
4413            };
4414            let api: api::stream::ReadStart = ReadStart::new()
4415                .with_from(from)
4416                .with_clamp_to_tail(clamp_to_tail)
4417                .into();
4418
4419            prop_assert_eq!(api.seq_num, seq_num);
4420            prop_assert_eq!(api.timestamp, timestamp);
4421            prop_assert_eq!(api.tail_offset, tail_offset);
4422            prop_assert_eq!(api.clamp, clamp_to_tail.then_some(true));
4423        }
4424    }
4425
4426    // -- ReadStop --
4427
4428    #[test]
4429    fn read_stop_to_api() {
4430        let stop = ReadStop::new()
4431            .with_limits(ReadLimits::new().with_count(50))
4432            .with_until(..1000)
4433            .with_wait(30);
4434        let api: api::stream::ReadEnd = stop.into();
4435        assert_eq!(api.count, Some(50));
4436        assert_eq!(api.until, Some(1000));
4437        assert_eq!(api.wait, Some(30));
4438    }
4439
4440    // -- Operation roundtrip --
4441
4442    #[test]
4443    fn operation_roundtrip_all_variants() {
4444        let variants = [
4445            Operation::ListBasins,
4446            Operation::CreateBasin,
4447            Operation::GetBasinConfig,
4448            Operation::DeleteBasin,
4449            Operation::ReconfigureBasin,
4450            Operation::ListAccessTokens,
4451            Operation::IssueAccessToken,
4452            Operation::RevokeAccessToken,
4453            Operation::GetAccountMetrics,
4454            Operation::GetBasinMetrics,
4455            Operation::GetStreamMetrics,
4456            Operation::ListStreams,
4457            Operation::CreateStream,
4458            Operation::GetStreamConfig,
4459            Operation::DeleteStream,
4460            Operation::ReconfigureStream,
4461            Operation::CheckTail,
4462            Operation::Append,
4463            Operation::Read,
4464            Operation::Trim,
4465            Operation::Fence,
4466            Operation::ListLocations,
4467            Operation::GetDefaultLocation,
4468            Operation::SetDefaultLocation,
4469        ];
4470        for op in variants {
4471            let api_op: api::access::Operation = op.into();
4472            let back: Operation = api_op.into();
4473            assert_eq!(back, op);
4474        }
4475    }
4476
4477    // -- MetricUnit --
4478
4479    #[test]
4480    fn metric_unit_conversion() {
4481        assert_eq!(
4482            MetricUnit::from(api::metrics::MetricUnit::Bytes),
4483            MetricUnit::Bytes
4484        );
4485        assert_eq!(
4486            MetricUnit::from(api::metrics::MetricUnit::Operations),
4487            MetricUnit::Operations
4488        );
4489    }
4490
4491    // -- AppendAck --
4492
4493    proptest! {
4494        #[test]
4495        fn append_ack_from_proto_preserves_present_positions_and_defaults_missing(
4496            start in proptest::option::of(proto_stream_position_strategy()),
4497            end in proptest::option::of(proto_stream_position_strategy()),
4498            tail in proptest::option::of(proto_stream_position_strategy()),
4499        ) {
4500            let expected_start = start.unwrap_or_default();
4501            let expected_end = end.unwrap_or_default();
4502            let expected_tail = tail.unwrap_or_default();
4503            let ack: AppendAck = api::stream::proto::AppendAck { start, end, tail }.into();
4504
4505            prop_assert_eq!(ack.start.seq_num, expected_start.seq_num);
4506            prop_assert_eq!(ack.start.timestamp, expected_start.timestamp);
4507            prop_assert_eq!(ack.end.seq_num, expected_end.seq_num);
4508            prop_assert_eq!(ack.end.timestamp, expected_end.timestamp);
4509            prop_assert_eq!(ack.tail.seq_num, expected_tail.seq_num);
4510            prop_assert_eq!(ack.tail.timestamp, expected_tail.timestamp);
4511        }
4512    }
4513
4514    // -- ReadBatch --
4515
4516    #[test]
4517    fn read_batch_from_api() {
4518        let proto_batch = api::stream::proto::ReadBatch {
4519            records: vec![api::stream::proto::SequencedRecord {
4520                seq_num: 0,
4521                body: Bytes::from("hi"),
4522                headers: vec![api::stream::proto::Header {
4523                    name: Bytes::from("k"),
4524                    value: Bytes::from("v"),
4525                }],
4526                timestamp: 42,
4527            }],
4528            tail: Some(api::stream::proto::StreamPosition {
4529                seq_num: 1,
4530                timestamp: 42,
4531            }),
4532        };
4533        let batch = ReadBatch::from_api(proto_batch);
4534        assert_eq!(batch.records.len(), 1);
4535        assert_eq!(batch.records[0].seq_num, 0);
4536        assert_eq!(batch.records[0].timestamp, 42);
4537        assert_eq!(batch.records[0].body.as_ref(), b"hi");
4538        assert_eq!(batch.records[0].headers.len(), 1);
4539        assert_eq!(batch.records[0].headers[0].name.as_ref(), b"k");
4540        assert_eq!(batch.records[0].headers[0].value.as_ref(), b"v");
4541        assert_eq!(
4542            batch.tail,
4543            Some(StreamPosition {
4544                seq_num: 1,
4545                timestamp: 42,
4546            })
4547        );
4548    }
4549
4550    // -- CreateBasinInput --
4551
4552    #[test]
4553    fn create_basin_input_to_api() {
4554        let name: BasinName = "test-basin-name".parse().unwrap();
4555        let input = CreateBasinInput::new(name.clone()).with_config(BasinConfig::new());
4556        let (req, token): (api::basin::CreateBasinRequest, String) = input.into();
4557        assert_eq!(req.basin, name);
4558        assert!(req.config.is_some());
4559        assert!(!token.is_empty());
4560    }
4561
4562    // -- CreateStreamInput --
4563
4564    #[test]
4565    fn create_stream_input_to_api() {
4566        let name: StreamName = "my-stream".parse().unwrap();
4567        let input = CreateStreamInput::new(name.clone()).with_config(StreamConfig::new());
4568        let (req, token): (api::stream::CreateStreamRequest, String) = input.into();
4569        assert_eq!(req.stream, name);
4570        assert!(req.config.is_some());
4571        assert!(!token.is_empty());
4572    }
4573
4574    // -- SequencedRecord from proto --
4575
4576    #[test]
4577    fn sequenced_record_from_proto() {
4578        let proto = api::stream::proto::SequencedRecord {
4579            seq_num: 99,
4580            body: Bytes::from("data"),
4581            headers: vec![api::stream::proto::Header {
4582                name: Bytes::from("k"),
4583                value: Bytes::from("v"),
4584            }],
4585            timestamp: 1234,
4586        };
4587        let record: SequencedRecord = proto.into();
4588        assert_eq!(record.seq_num, 99);
4589        assert_eq!(record.body.as_ref(), b"data");
4590        assert_eq!(record.headers.len(), 1);
4591        assert_eq!(record.headers[0].name.as_ref(), b"k");
4592        assert_eq!(record.headers[0].value.as_ref(), b"v");
4593        assert_eq!(record.timestamp, 1234);
4594    }
4595}