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