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