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