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: "a.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, or `None` if the token does not expire.
1566    pub expires_at: Option<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.expires_at.map(S2DateTime::try_from).transpose()?;
1579        Ok(Self {
1580            id: value.id,
1581            expires_at,
1582            auto_prefix_streams: value.auto_prefix_streams,
1583            scope: value.scope.into(),
1584        })
1585    }
1586}
1587
1588#[derive(Debug, Clone)]
1589/// Pattern for matching basins.
1590///
1591/// See [`AccessTokenScope::basins`].
1592pub enum BasinMatcher {
1593    /// Match no basins.
1594    None,
1595    /// Match exactly this basin.
1596    Exact(BasinName),
1597    /// Match all basins with this prefix.
1598    Prefix(BasinNamePrefix),
1599}
1600
1601#[derive(Debug, Clone)]
1602/// Pattern for matching streams.
1603///
1604/// See [`AccessTokenScope::streams`].
1605pub enum StreamMatcher {
1606    /// Match no streams.
1607    None,
1608    /// Match exactly this stream.
1609    Exact(StreamName),
1610    /// Match all streams with this prefix.
1611    Prefix(StreamNamePrefix),
1612}
1613
1614#[derive(Debug, Clone)]
1615/// Pattern for matching access tokens.
1616///
1617/// See [`AccessTokenScope::access_tokens`].
1618pub enum AccessTokenMatcher {
1619    /// Match no access tokens.
1620    None,
1621    /// Match exactly this access token.
1622    Exact(AccessTokenId),
1623    /// Match all access tokens with this prefix.
1624    Prefix(AccessTokenIdPrefix),
1625}
1626
1627#[derive(Debug, Clone, Default)]
1628#[non_exhaustive]
1629/// Permissions indicating allowed operations.
1630pub struct ReadWritePermissions {
1631    /// Read permission.
1632    ///
1633    /// Defaults to `false`.
1634    pub read: bool,
1635    /// Write permission.
1636    ///
1637    /// Defaults to `false`.
1638    pub write: bool,
1639}
1640
1641impl ReadWritePermissions {
1642    /// Create a new [`ReadWritePermissions`] with default values.
1643    pub fn new() -> Self {
1644        Self::default()
1645    }
1646
1647    /// Create read-only permissions.
1648    pub fn read_only() -> Self {
1649        Self {
1650            read: true,
1651            write: false,
1652        }
1653    }
1654
1655    /// Create write-only permissions.
1656    pub fn write_only() -> Self {
1657        Self {
1658            read: false,
1659            write: true,
1660        }
1661    }
1662
1663    /// Create read-write permissions.
1664    pub fn read_write() -> Self {
1665        Self {
1666            read: true,
1667            write: true,
1668        }
1669    }
1670}
1671
1672impl From<ReadWritePermissions> for api::access::ReadWritePermissions {
1673    fn from(value: ReadWritePermissions) -> Self {
1674        Self {
1675            read: Some(value.read),
1676            write: Some(value.write),
1677        }
1678    }
1679}
1680
1681impl From<api::access::ReadWritePermissions> for ReadWritePermissions {
1682    fn from(value: api::access::ReadWritePermissions) -> Self {
1683        Self {
1684            read: value.read.unwrap_or_default(),
1685            write: value.write.unwrap_or_default(),
1686        }
1687    }
1688}
1689
1690#[derive(Debug, Clone, Default)]
1691#[non_exhaustive]
1692/// Permissions at the operation group level.
1693///
1694/// See [`AccessTokenScope::op_group_perms`].
1695pub struct OperationGroupPermissions {
1696    /// Account-level access permissions.
1697    ///
1698    /// Defaults to `None`.
1699    pub account: Option<ReadWritePermissions>,
1700    /// Basin-level access permissions.
1701    ///
1702    /// Defaults to `None`.
1703    pub basin: Option<ReadWritePermissions>,
1704    /// Stream-level access permissions.
1705    ///
1706    /// Defaults to `None`.
1707    pub stream: Option<ReadWritePermissions>,
1708}
1709
1710impl OperationGroupPermissions {
1711    /// Create a new [`OperationGroupPermissions`] with default values.
1712    pub fn new() -> Self {
1713        Self::default()
1714    }
1715
1716    /// Create read-only permissions for all groups.
1717    pub fn read_only_all() -> Self {
1718        Self {
1719            account: Some(ReadWritePermissions::read_only()),
1720            basin: Some(ReadWritePermissions::read_only()),
1721            stream: Some(ReadWritePermissions::read_only()),
1722        }
1723    }
1724
1725    /// Create write-only permissions for all groups.
1726    pub fn write_only_all() -> Self {
1727        Self {
1728            account: Some(ReadWritePermissions::write_only()),
1729            basin: Some(ReadWritePermissions::write_only()),
1730            stream: Some(ReadWritePermissions::write_only()),
1731        }
1732    }
1733
1734    /// Create read-write permissions for all groups.
1735    pub fn read_write_all() -> Self {
1736        Self {
1737            account: Some(ReadWritePermissions::read_write()),
1738            basin: Some(ReadWritePermissions::read_write()),
1739            stream: Some(ReadWritePermissions::read_write()),
1740        }
1741    }
1742
1743    /// Set account-level access permissions.
1744    pub fn with_account(self, account: ReadWritePermissions) -> Self {
1745        Self {
1746            account: Some(account),
1747            ..self
1748        }
1749    }
1750
1751    /// Set basin-level access permissions.
1752    pub fn with_basin(self, basin: ReadWritePermissions) -> Self {
1753        Self {
1754            basin: Some(basin),
1755            ..self
1756        }
1757    }
1758
1759    /// Set stream-level access permissions.
1760    pub fn with_stream(self, stream: ReadWritePermissions) -> Self {
1761        Self {
1762            stream: Some(stream),
1763            ..self
1764        }
1765    }
1766}
1767
1768impl From<OperationGroupPermissions> for api::access::PermittedOperationGroups {
1769    fn from(value: OperationGroupPermissions) -> Self {
1770        Self {
1771            account: value.account.map(Into::into),
1772            basin: value.basin.map(Into::into),
1773            stream: value.stream.map(Into::into),
1774        }
1775    }
1776}
1777
1778impl From<api::access::PermittedOperationGroups> for OperationGroupPermissions {
1779    fn from(value: api::access::PermittedOperationGroups) -> Self {
1780        Self {
1781            account: value.account.map(Into::into),
1782            basin: value.basin.map(Into::into),
1783            stream: value.stream.map(Into::into),
1784        }
1785    }
1786}
1787
1788#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
1789/// Individual operation that can be permitted.
1790///
1791/// See [`AccessTokenScope::ops`].
1792pub enum Operation {
1793    /// List basins.
1794    ListBasins,
1795    /// Create a basin.
1796    CreateBasin,
1797    /// Get basin configuration.
1798    GetBasinConfig,
1799    /// Delete a basin.
1800    DeleteBasin,
1801    /// Reconfigure a basin.
1802    ReconfigureBasin,
1803    /// List access tokens.
1804    ListAccessTokens,
1805    /// Issue an access token.
1806    IssueAccessToken,
1807    /// Revoke an access token.
1808    RevokeAccessToken,
1809    /// Get account metrics.
1810    GetAccountMetrics,
1811    /// Get basin metrics.
1812    GetBasinMetrics,
1813    /// Get stream metrics.
1814    GetStreamMetrics,
1815    /// List streams.
1816    ListStreams,
1817    /// Create a stream.
1818    CreateStream,
1819    /// Get stream configuration.
1820    GetStreamConfig,
1821    /// Delete a stream.
1822    DeleteStream,
1823    /// Reconfigure a stream.
1824    ReconfigureStream,
1825    /// Check the tail of a stream.
1826    CheckTail,
1827    /// Append records to a stream.
1828    Append,
1829    /// Read records from a stream.
1830    Read,
1831    /// Trim records on a stream.
1832    Trim,
1833    /// Set the fencing token on a stream.
1834    Fence,
1835    /// List locations.
1836    ListLocations,
1837    /// Get the default location.
1838    GetDefaultLocation,
1839    /// Set the default location.
1840    SetDefaultLocation,
1841}
1842
1843impl From<Operation> for api::access::Operation {
1844    fn from(value: Operation) -> Self {
1845        match value {
1846            Operation::ListBasins => api::access::Operation::ListBasins,
1847            Operation::CreateBasin => api::access::Operation::CreateBasin,
1848            Operation::DeleteBasin => api::access::Operation::DeleteBasin,
1849            Operation::ReconfigureBasin => api::access::Operation::ReconfigureBasin,
1850            Operation::GetBasinConfig => api::access::Operation::GetBasinConfig,
1851            Operation::IssueAccessToken => api::access::Operation::IssueAccessToken,
1852            Operation::RevokeAccessToken => api::access::Operation::RevokeAccessToken,
1853            Operation::ListAccessTokens => api::access::Operation::ListAccessTokens,
1854            Operation::ListStreams => api::access::Operation::ListStreams,
1855            Operation::CreateStream => api::access::Operation::CreateStream,
1856            Operation::DeleteStream => api::access::Operation::DeleteStream,
1857            Operation::GetStreamConfig => api::access::Operation::GetStreamConfig,
1858            Operation::ReconfigureStream => api::access::Operation::ReconfigureStream,
1859            Operation::CheckTail => api::access::Operation::CheckTail,
1860            Operation::Append => api::access::Operation::Append,
1861            Operation::Read => api::access::Operation::Read,
1862            Operation::Trim => api::access::Operation::Trim,
1863            Operation::Fence => api::access::Operation::Fence,
1864            Operation::GetAccountMetrics => api::access::Operation::AccountMetrics,
1865            Operation::GetBasinMetrics => api::access::Operation::BasinMetrics,
1866            Operation::GetStreamMetrics => api::access::Operation::StreamMetrics,
1867            Operation::ListLocations => api::access::Operation::ListLocations,
1868            Operation::GetDefaultLocation => api::access::Operation::GetDefaultLocation,
1869            Operation::SetDefaultLocation => api::access::Operation::SetDefaultLocation,
1870        }
1871    }
1872}
1873
1874impl From<api::access::Operation> for Operation {
1875    fn from(value: api::access::Operation) -> Self {
1876        match value {
1877            api::access::Operation::ListBasins => Operation::ListBasins,
1878            api::access::Operation::CreateBasin => Operation::CreateBasin,
1879            api::access::Operation::DeleteBasin => Operation::DeleteBasin,
1880            api::access::Operation::ReconfigureBasin => Operation::ReconfigureBasin,
1881            api::access::Operation::GetBasinConfig => Operation::GetBasinConfig,
1882            api::access::Operation::IssueAccessToken => Operation::IssueAccessToken,
1883            api::access::Operation::RevokeAccessToken => Operation::RevokeAccessToken,
1884            api::access::Operation::ListAccessTokens => Operation::ListAccessTokens,
1885            api::access::Operation::ListStreams => Operation::ListStreams,
1886            api::access::Operation::CreateStream => Operation::CreateStream,
1887            api::access::Operation::DeleteStream => Operation::DeleteStream,
1888            api::access::Operation::GetStreamConfig => Operation::GetStreamConfig,
1889            api::access::Operation::ReconfigureStream => Operation::ReconfigureStream,
1890            api::access::Operation::CheckTail => Operation::CheckTail,
1891            api::access::Operation::Append => Operation::Append,
1892            api::access::Operation::Read => Operation::Read,
1893            api::access::Operation::Trim => Operation::Trim,
1894            api::access::Operation::Fence => Operation::Fence,
1895            api::access::Operation::AccountMetrics => Operation::GetAccountMetrics,
1896            api::access::Operation::BasinMetrics => Operation::GetBasinMetrics,
1897            api::access::Operation::StreamMetrics => Operation::GetStreamMetrics,
1898            api::access::Operation::ListLocations => Operation::ListLocations,
1899            api::access::Operation::GetDefaultLocation => Operation::GetDefaultLocation,
1900            api::access::Operation::SetDefaultLocation => Operation::SetDefaultLocation,
1901        }
1902    }
1903}
1904
1905#[derive(Debug, Clone)]
1906#[non_exhaustive]
1907/// Scope of an access token.
1908///
1909/// **Note:** The final set of permitted operations is the union of [`ops`](AccessTokenScope::ops)
1910/// and the operations permitted by [`op_group_perms`](AccessTokenScope::op_group_perms). Also, the
1911/// final set must not be empty.
1912///
1913/// See [`IssueAccessTokenInput::scope`].
1914pub struct AccessTokenScopeInput {
1915    basins: Option<BasinMatcher>,
1916    streams: Option<StreamMatcher>,
1917    access_tokens: Option<AccessTokenMatcher>,
1918    op_group_perms: Option<OperationGroupPermissions>,
1919    ops: HashSet<Operation>,
1920}
1921
1922impl AccessTokenScopeInput {
1923    /// Create a new [`AccessTokenScopeInput`] with the given permitted operations.
1924    pub fn from_ops(ops: impl IntoIterator<Item = Operation>) -> Self {
1925        Self {
1926            basins: None,
1927            streams: None,
1928            access_tokens: None,
1929            op_group_perms: None,
1930            ops: ops.into_iter().collect(),
1931        }
1932    }
1933
1934    /// Create a new [`AccessTokenScopeInput`] with the given operation group permissions.
1935    pub fn from_op_group_perms(op_group_perms: OperationGroupPermissions) -> Self {
1936        Self {
1937            basins: None,
1938            streams: None,
1939            access_tokens: None,
1940            op_group_perms: Some(op_group_perms),
1941            ops: HashSet::default(),
1942        }
1943    }
1944
1945    /// Set the permitted operations.
1946    pub fn with_ops(self, ops: impl IntoIterator<Item = Operation>) -> Self {
1947        Self {
1948            ops: ops.into_iter().collect(),
1949            ..self
1950        }
1951    }
1952
1953    /// Set the access permissions at the operation group level.
1954    pub fn with_op_group_perms(self, op_group_perms: OperationGroupPermissions) -> Self {
1955        Self {
1956            op_group_perms: Some(op_group_perms),
1957            ..self
1958        }
1959    }
1960
1961    /// Set the permitted basins.
1962    ///
1963    /// Defaults to no basins.
1964    pub fn with_basins(self, basins: BasinMatcher) -> Self {
1965        Self {
1966            basins: Some(basins),
1967            ..self
1968        }
1969    }
1970
1971    /// Set the permitted streams.
1972    ///
1973    /// Defaults to no streams.
1974    pub fn with_streams(self, streams: StreamMatcher) -> Self {
1975        Self {
1976            streams: Some(streams),
1977            ..self
1978        }
1979    }
1980
1981    /// Set the permitted access tokens.
1982    ///
1983    /// Defaults to no access tokens.
1984    pub fn with_access_tokens(self, access_tokens: AccessTokenMatcher) -> Self {
1985        Self {
1986            access_tokens: Some(access_tokens),
1987            ..self
1988        }
1989    }
1990}
1991
1992#[derive(Debug, Clone)]
1993#[non_exhaustive]
1994/// Scope of an access token.
1995pub struct AccessTokenScope {
1996    /// Permitted basins.
1997    pub basins: Option<BasinMatcher>,
1998    /// Permitted streams.
1999    pub streams: Option<StreamMatcher>,
2000    /// Permitted access tokens.
2001    pub access_tokens: Option<AccessTokenMatcher>,
2002    /// Permissions at the operation group level.
2003    pub op_group_perms: Option<OperationGroupPermissions>,
2004    /// Permitted operations.
2005    pub ops: HashSet<Operation>,
2006}
2007
2008impl From<api::access::AccessTokenScope> for AccessTokenScope {
2009    fn from(value: api::access::AccessTokenScope) -> Self {
2010        Self {
2011            basins: value.basins.map(|rs| match rs {
2012                api::access::ResourceSet::Exact(api::access::MaybeEmpty::NonEmpty(e)) => {
2013                    BasinMatcher::Exact(e)
2014                }
2015                api::access::ResourceSet::Exact(api::access::MaybeEmpty::Empty) => {
2016                    BasinMatcher::None
2017                }
2018                api::access::ResourceSet::Prefix(p) => BasinMatcher::Prefix(p),
2019            }),
2020            streams: value.streams.map(|rs| match rs {
2021                api::access::ResourceSet::Exact(api::access::MaybeEmpty::NonEmpty(e)) => {
2022                    StreamMatcher::Exact(e)
2023                }
2024                api::access::ResourceSet::Exact(api::access::MaybeEmpty::Empty) => {
2025                    StreamMatcher::None
2026                }
2027                api::access::ResourceSet::Prefix(p) => StreamMatcher::Prefix(p),
2028            }),
2029            access_tokens: value.access_tokens.map(|rs| match rs {
2030                api::access::ResourceSet::Exact(api::access::MaybeEmpty::NonEmpty(e)) => {
2031                    AccessTokenMatcher::Exact(e)
2032                }
2033                api::access::ResourceSet::Exact(api::access::MaybeEmpty::Empty) => {
2034                    AccessTokenMatcher::None
2035                }
2036                api::access::ResourceSet::Prefix(p) => AccessTokenMatcher::Prefix(p),
2037            }),
2038            op_group_perms: value.op_groups.map(Into::into),
2039            ops: value
2040                .ops
2041                .map(|ops| ops.into_iter().map(Into::into).collect())
2042                .unwrap_or_default(),
2043        }
2044    }
2045}
2046
2047impl From<AccessTokenScopeInput> for api::access::AccessTokenScope {
2048    fn from(value: AccessTokenScopeInput) -> Self {
2049        Self {
2050            basins: value.basins.map(|rs| match rs {
2051                BasinMatcher::None => {
2052                    api::access::ResourceSet::Exact(api::access::MaybeEmpty::Empty)
2053                }
2054                BasinMatcher::Exact(e) => {
2055                    api::access::ResourceSet::Exact(api::access::MaybeEmpty::NonEmpty(e))
2056                }
2057                BasinMatcher::Prefix(p) => api::access::ResourceSet::Prefix(p),
2058            }),
2059            streams: value.streams.map(|rs| match rs {
2060                StreamMatcher::None => {
2061                    api::access::ResourceSet::Exact(api::access::MaybeEmpty::Empty)
2062                }
2063                StreamMatcher::Exact(e) => {
2064                    api::access::ResourceSet::Exact(api::access::MaybeEmpty::NonEmpty(e))
2065                }
2066                StreamMatcher::Prefix(p) => api::access::ResourceSet::Prefix(p),
2067            }),
2068            access_tokens: value.access_tokens.map(|rs| match rs {
2069                AccessTokenMatcher::None => {
2070                    api::access::ResourceSet::Exact(api::access::MaybeEmpty::Empty)
2071                }
2072                AccessTokenMatcher::Exact(e) => {
2073                    api::access::ResourceSet::Exact(api::access::MaybeEmpty::NonEmpty(e))
2074                }
2075                AccessTokenMatcher::Prefix(p) => api::access::ResourceSet::Prefix(p),
2076            }),
2077            op_groups: value.op_group_perms.map(Into::into),
2078            ops: if value.ops.is_empty() {
2079                None
2080            } else {
2081                Some(value.ops.into_iter().map(Into::into).collect())
2082            },
2083        }
2084    }
2085}
2086
2087#[derive(Debug, Clone)]
2088#[non_exhaustive]
2089/// Input for [`issue_access_token`](crate::S2::issue_access_token).
2090pub struct IssueAccessTokenInput {
2091    /// Access token ID.
2092    pub id: AccessTokenId,
2093    /// Expiration time.
2094    ///
2095    /// Defaults to the expiration time of requestor's access token passed via
2096    /// [`S2Config`](S2Config::new).
2097    pub expires_at: Option<S2DateTime>,
2098    /// Whether to automatically prefix stream names during creation and strip the prefix during
2099    /// listing.
2100    ///
2101    /// **Note:** [`scope.streams`](AccessTokenScopeInput::with_streams) must be set with the
2102    /// prefix.
2103    ///
2104    /// Defaults to `false`.
2105    pub auto_prefix_streams: bool,
2106    /// Scope of the token.
2107    pub scope: AccessTokenScopeInput,
2108}
2109
2110impl IssueAccessTokenInput {
2111    /// Create a new [`IssueAccessTokenInput`] with the given id and scope.
2112    pub fn new(id: AccessTokenId, scope: AccessTokenScopeInput) -> Self {
2113        Self {
2114            id,
2115            expires_at: None,
2116            auto_prefix_streams: false,
2117            scope,
2118        }
2119    }
2120
2121    /// Set the expiration time.
2122    pub fn with_expires_at(self, expires_at: S2DateTime) -> Self {
2123        Self {
2124            expires_at: Some(expires_at),
2125            ..self
2126        }
2127    }
2128
2129    /// Set whether to automatically prefix stream names during creation and strip the prefix during
2130    /// listing.
2131    pub fn with_auto_prefix_streams(self, auto_prefix_streams: bool) -> Self {
2132        Self {
2133            auto_prefix_streams,
2134            ..self
2135        }
2136    }
2137}
2138
2139impl From<IssueAccessTokenInput> for api::access::IssueAccessTokenRequest {
2140    fn from(value: IssueAccessTokenInput) -> Self {
2141        Self {
2142            id: value.id,
2143            expires_at: value.expires_at.map(Into::into),
2144            auto_prefix_streams: value.auto_prefix_streams.then_some(true),
2145            scope: value.scope.into(),
2146        }
2147    }
2148}
2149
2150#[derive(Debug, Clone, Copy, PartialEq, Eq)]
2151/// Interval to accumulate over for timeseries metric sets.
2152pub enum TimeseriesInterval {
2153    /// Minute.
2154    Minute,
2155    /// Hour.
2156    Hour,
2157    /// Day.
2158    Day,
2159}
2160
2161impl From<TimeseriesInterval> for api::metrics::TimeseriesInterval {
2162    fn from(value: TimeseriesInterval) -> Self {
2163        match value {
2164            TimeseriesInterval::Minute => api::metrics::TimeseriesInterval::Minute,
2165            TimeseriesInterval::Hour => api::metrics::TimeseriesInterval::Hour,
2166            TimeseriesInterval::Day => api::metrics::TimeseriesInterval::Day,
2167        }
2168    }
2169}
2170
2171impl From<api::metrics::TimeseriesInterval> for TimeseriesInterval {
2172    fn from(value: api::metrics::TimeseriesInterval) -> Self {
2173        match value {
2174            api::metrics::TimeseriesInterval::Minute => TimeseriesInterval::Minute,
2175            api::metrics::TimeseriesInterval::Hour => TimeseriesInterval::Hour,
2176            api::metrics::TimeseriesInterval::Day => TimeseriesInterval::Day,
2177        }
2178    }
2179}
2180
2181#[derive(Debug, Clone, Copy)]
2182#[non_exhaustive]
2183/// Time range as Unix epoch seconds.
2184pub struct TimeRange {
2185    /// Start timestamp (inclusive).
2186    pub start: u32,
2187    /// End timestamp (exclusive).
2188    pub end: u32,
2189}
2190
2191impl TimeRange {
2192    /// Create a new [`TimeRange`] with the given start and end timestamps.
2193    pub fn new(start: u32, end: u32) -> Self {
2194        Self { start, end }
2195    }
2196}
2197
2198#[derive(Debug, Clone, Copy)]
2199#[non_exhaustive]
2200/// Time range as Unix epoch seconds and accumulation interval.
2201pub struct TimeRangeAndInterval {
2202    /// Start timestamp (inclusive).
2203    pub start: u32,
2204    /// End timestamp (exclusive).
2205    pub end: u32,
2206    /// Interval to accumulate over for timeseries metric sets.
2207    ///
2208    /// Default is dependent on the requested metric set.
2209    pub interval: Option<TimeseriesInterval>,
2210}
2211
2212impl TimeRangeAndInterval {
2213    /// Create a new [`TimeRangeAndInterval`] with the given start and end timestamps.
2214    pub fn new(start: u32, end: u32) -> Self {
2215        Self {
2216            start,
2217            end,
2218            interval: None,
2219        }
2220    }
2221
2222    /// Set the interval to accumulate over for timeseries metric sets.
2223    pub fn with_interval(self, interval: TimeseriesInterval) -> Self {
2224        Self {
2225            interval: Some(interval),
2226            ..self
2227        }
2228    }
2229}
2230
2231#[derive(Debug, Clone, Copy)]
2232/// Account metric set to return.
2233pub enum AccountMetricSet {
2234    /// Returns a [`LabelMetric`] representing all basins which had at least one stream within the
2235    /// specified time range.
2236    ActiveBasins(TimeRange),
2237    /// Returns [`AccumulationMetric`]s, one per account operation type.
2238    ///
2239    /// Each metric represents a timeseries of the number of operations, with one accumulated value
2240    /// per interval over the requested time range.
2241    ///
2242    /// [`interval`](TimeRangeAndInterval::interval) defaults to [`hour`](TimeseriesInterval::Hour).
2243    AccountOps(TimeRangeAndInterval),
2244}
2245
2246#[derive(Debug, Clone)]
2247#[non_exhaustive]
2248/// Input for [`get_account_metrics`](crate::S2::get_account_metrics) operation.
2249pub struct GetAccountMetricsInput {
2250    /// Metric set to return.
2251    pub set: AccountMetricSet,
2252}
2253
2254impl GetAccountMetricsInput {
2255    /// Create a new [`GetAccountMetricsInput`] with the given account metric set.
2256    pub fn new(set: AccountMetricSet) -> Self {
2257        Self { set }
2258    }
2259}
2260
2261impl From<GetAccountMetricsInput> for api::metrics::AccountMetricSetRequest {
2262    fn from(value: GetAccountMetricsInput) -> Self {
2263        let (set, start, end, interval) = match value.set {
2264            AccountMetricSet::ActiveBasins(args) => (
2265                api::metrics::AccountMetricSet::ActiveBasins,
2266                args.start,
2267                args.end,
2268                None,
2269            ),
2270            AccountMetricSet::AccountOps(args) => (
2271                api::metrics::AccountMetricSet::AccountOps,
2272                args.start,
2273                args.end,
2274                args.interval,
2275            ),
2276        };
2277        Self {
2278            set,
2279            start: Some(start),
2280            end: Some(end),
2281            interval: interval.map(Into::into),
2282        }
2283    }
2284}
2285
2286#[derive(Debug, Clone, Copy)]
2287/// Basin metric set to return.
2288pub enum BasinMetricSet {
2289    /// Returns a [`GaugeMetric`] representing a timeseries of total stored bytes across all streams
2290    /// in the basin, with one observed value for each hour over the requested time range.
2291    Storage(TimeRange),
2292    /// Returns [`AccumulationMetric`]s, one per storage class (standard, express).
2293    ///
2294    /// Each metric represents a timeseries of the number of append operations across all streams
2295    /// in the basin, with one accumulated value per interval over the requested time range.
2296    ///
2297    /// [`interval`](TimeRangeAndInterval::interval) defaults to
2298    /// [`minute`](TimeseriesInterval::Minute).
2299    AppendOps(TimeRangeAndInterval),
2300    /// Returns [`AccumulationMetric`]s, one per read type (unary, streaming).
2301    ///
2302    /// Each metric represents a timeseries of the number of read operations across all streams
2303    /// in the basin, with one accumulated value per interval over the requested time range.
2304    ///
2305    /// [`interval`](TimeRangeAndInterval::interval) defaults to
2306    /// [`minute`](TimeseriesInterval::Minute).
2307    ReadOps(TimeRangeAndInterval),
2308    /// Returns an [`AccumulationMetric`] representing a timeseries of total read bytes
2309    /// across all streams in the basin, with one accumulated value per interval
2310    /// over the requested time range.
2311    ///
2312    /// [`interval`](TimeRangeAndInterval::interval) defaults to
2313    /// [`minute`](TimeseriesInterval::Minute).
2314    ReadThroughput(TimeRangeAndInterval),
2315    /// Returns an [`AccumulationMetric`] representing a timeseries of total appended bytes
2316    /// across all streams in the basin, with one accumulated value per interval
2317    /// over the requested time range.
2318    ///
2319    /// [`interval`](TimeRangeAndInterval::interval) defaults to
2320    /// [`minute`](TimeseriesInterval::Minute).
2321    AppendThroughput(TimeRangeAndInterval),
2322    /// Returns [`AccumulationMetric`]s, one per basin operation type.
2323    ///
2324    /// Each metric represents a timeseries of the number of operations, with one accumulated value
2325    /// per interval over the requested time range.
2326    ///
2327    /// [`interval`](TimeRangeAndInterval::interval) defaults to [`hour`](TimeseriesInterval::Hour).
2328    BasinOps(TimeRangeAndInterval),
2329}
2330
2331#[derive(Debug, Clone)]
2332#[non_exhaustive]
2333/// Input for [`get_basin_metrics`](crate::S2::get_basin_metrics) operation.
2334pub struct GetBasinMetricsInput {
2335    /// Basin name.
2336    pub name: BasinName,
2337    /// Metric set to return.
2338    pub set: BasinMetricSet,
2339}
2340
2341impl GetBasinMetricsInput {
2342    /// Create a new [`GetBasinMetricsInput`] with the given basin name and metric set.
2343    pub fn new(name: BasinName, set: BasinMetricSet) -> Self {
2344        Self { name, set }
2345    }
2346}
2347
2348impl From<GetBasinMetricsInput> for (BasinName, api::metrics::BasinMetricSetRequest) {
2349    fn from(value: GetBasinMetricsInput) -> Self {
2350        let (set, start, end, interval) = match value.set {
2351            BasinMetricSet::Storage(args) => (
2352                api::metrics::BasinMetricSet::Storage,
2353                args.start,
2354                args.end,
2355                None,
2356            ),
2357            BasinMetricSet::AppendOps(args) => (
2358                api::metrics::BasinMetricSet::AppendOps,
2359                args.start,
2360                args.end,
2361                args.interval,
2362            ),
2363            BasinMetricSet::ReadOps(args) => (
2364                api::metrics::BasinMetricSet::ReadOps,
2365                args.start,
2366                args.end,
2367                args.interval,
2368            ),
2369            BasinMetricSet::ReadThroughput(args) => (
2370                api::metrics::BasinMetricSet::ReadThroughput,
2371                args.start,
2372                args.end,
2373                args.interval,
2374            ),
2375            BasinMetricSet::AppendThroughput(args) => (
2376                api::metrics::BasinMetricSet::AppendThroughput,
2377                args.start,
2378                args.end,
2379                args.interval,
2380            ),
2381            BasinMetricSet::BasinOps(args) => (
2382                api::metrics::BasinMetricSet::BasinOps,
2383                args.start,
2384                args.end,
2385                args.interval,
2386            ),
2387        };
2388        (
2389            value.name,
2390            api::metrics::BasinMetricSetRequest {
2391                set,
2392                start: Some(start),
2393                end: Some(end),
2394                interval: interval.map(Into::into),
2395            },
2396        )
2397    }
2398}
2399
2400#[derive(Debug, Clone, Copy)]
2401/// Stream metric set to return.
2402pub enum StreamMetricSet {
2403    /// Returns a [`GaugeMetric`] representing a timeseries of total stored bytes for the stream,
2404    /// with one observed value for each minute over the requested time range.
2405    Storage(TimeRange),
2406}
2407
2408#[derive(Debug, Clone)]
2409#[non_exhaustive]
2410/// Input for [`get_stream_metrics`](crate::S2::get_stream_metrics) operation.
2411pub struct GetStreamMetricsInput {
2412    /// Basin name.
2413    pub basin_name: BasinName,
2414    /// Stream name.
2415    pub stream_name: StreamName,
2416    /// Metric set to return.
2417    pub set: StreamMetricSet,
2418}
2419
2420impl GetStreamMetricsInput {
2421    /// Create a new [`GetStreamMetricsInput`] with the given basin name, stream name and metric
2422    /// set.
2423    pub fn new(basin_name: BasinName, stream_name: StreamName, set: StreamMetricSet) -> Self {
2424        Self {
2425            basin_name,
2426            stream_name,
2427            set,
2428        }
2429    }
2430}
2431
2432impl From<GetStreamMetricsInput> for (BasinName, StreamName, api::metrics::StreamMetricSetRequest) {
2433    fn from(value: GetStreamMetricsInput) -> Self {
2434        let (set, start, end, interval) = match value.set {
2435            StreamMetricSet::Storage(args) => (
2436                api::metrics::StreamMetricSet::Storage,
2437                args.start,
2438                args.end,
2439                None,
2440            ),
2441        };
2442        (
2443            value.basin_name,
2444            value.stream_name,
2445            api::metrics::StreamMetricSetRequest {
2446                set,
2447                start: Some(start),
2448                end: Some(end),
2449                interval,
2450            },
2451        )
2452    }
2453}
2454
2455#[derive(Debug, Clone, Copy, PartialEq, Eq)]
2456/// Unit in which metric values are measured.
2457pub enum MetricUnit {
2458    /// Size in bytes.
2459    Bytes,
2460    /// Number of operations.
2461    Operations,
2462}
2463
2464impl From<api::metrics::MetricUnit> for MetricUnit {
2465    fn from(value: api::metrics::MetricUnit) -> Self {
2466        match value {
2467            api::metrics::MetricUnit::Bytes => MetricUnit::Bytes,
2468            api::metrics::MetricUnit::Operations => MetricUnit::Operations,
2469        }
2470    }
2471}
2472
2473#[derive(Debug, Clone)]
2474#[non_exhaustive]
2475/// Single named value.
2476pub struct ScalarMetric {
2477    /// Metric name.
2478    pub name: String,
2479    /// Unit for the metric value.
2480    pub unit: MetricUnit,
2481    /// Metric value.
2482    pub value: f64,
2483}
2484
2485#[derive(Debug, Clone)]
2486#[non_exhaustive]
2487/// Named series of `(timestamp, value)` datapoints, each representing an accumulation over a
2488/// specified interval.
2489pub struct AccumulationMetric {
2490    /// Timeseries name.
2491    pub name: String,
2492    /// Unit for the accumulated values.
2493    pub unit: MetricUnit,
2494    /// The interval at which datapoints are accumulated.
2495    pub interval: TimeseriesInterval,
2496    /// Series of `(timestamp, value)` datapoints. Each datapoint represents the accumulated
2497    /// `value` for the time period starting at the `timestamp` (in Unix epoch seconds), spanning
2498    /// one `interval`.
2499    pub values: Vec<(u32, f64)>,
2500}
2501
2502#[derive(Debug, Clone)]
2503#[non_exhaustive]
2504/// Named series of `(timestamp, value)` datapoints, each representing an instantaneous value.
2505pub struct GaugeMetric {
2506    /// Timeseries name.
2507    pub name: String,
2508    /// Unit for the instantaneous values.
2509    pub unit: MetricUnit,
2510    /// Series of `(timestamp, value)` datapoints. Each datapoint represents the `value` at the
2511    /// instant of the `timestamp` (in Unix epoch seconds).
2512    pub values: Vec<(u32, f64)>,
2513}
2514
2515#[derive(Debug, Clone)]
2516#[non_exhaustive]
2517/// Set of string labels.
2518pub struct LabelMetric {
2519    /// Label name.
2520    pub name: String,
2521    /// Label values.
2522    pub values: Vec<String>,
2523}
2524
2525#[derive(Debug, Clone)]
2526/// Individual metric in a returned metric set.
2527pub enum Metric {
2528    /// Single named value.
2529    Scalar(ScalarMetric),
2530    /// Named series of `(timestamp, value)` datapoints, each representing an accumulation over a
2531    /// specified interval.
2532    Accumulation(AccumulationMetric),
2533    /// Named series of `(timestamp, value)` datapoints, each representing an instantaneous value.
2534    Gauge(GaugeMetric),
2535    /// Set of string labels.
2536    Label(LabelMetric),
2537}
2538
2539impl From<api::metrics::Metric> for Metric {
2540    fn from(value: api::metrics::Metric) -> Self {
2541        match value {
2542            api::metrics::Metric::Scalar(sm) => Metric::Scalar(ScalarMetric {
2543                name: sm.name.into(),
2544                unit: sm.unit.into(),
2545                value: sm.value,
2546            }),
2547            api::metrics::Metric::Accumulation(am) => Metric::Accumulation(AccumulationMetric {
2548                name: am.name.into(),
2549                unit: am.unit.into(),
2550                interval: am.interval.into(),
2551                values: am.values,
2552            }),
2553            api::metrics::Metric::Gauge(gm) => Metric::Gauge(GaugeMetric {
2554                name: gm.name.into(),
2555                unit: gm.unit.into(),
2556                values: gm.values,
2557            }),
2558            api::metrics::Metric::Label(lm) => Metric::Label(LabelMetric {
2559                name: lm.name.into(),
2560                values: lm.values,
2561            }),
2562        }
2563    }
2564}
2565
2566#[derive(Debug, Clone, Default)]
2567#[non_exhaustive]
2568/// Input for [`list_streams`](crate::S2Basin::list_streams) operation.
2569pub struct ListStreamsInput {
2570    /// Filter streams whose names begin with this value.
2571    ///
2572    /// Defaults to `""`.
2573    pub prefix: StreamNamePrefix,
2574    /// Filter streams whose names are lexicographically greater than this value.
2575    ///
2576    /// Defaults to `""`.
2577    pub start_after: StreamNameStartAfter,
2578    /// Number of streams to return in a page. Will be clamped to a maximum of `1000`.
2579    ///
2580    /// Defaults to `1000`.
2581    pub limit: Option<usize>,
2582}
2583
2584impl ListStreamsInput {
2585    /// Create a new [`ListStreamsInput`] with default values.
2586    pub fn new() -> Self {
2587        Self::default()
2588    }
2589
2590    /// Set the prefix used to filter streams whose names begin with this value.
2591    pub fn with_prefix(self, prefix: StreamNamePrefix) -> Self {
2592        Self { prefix, ..self }
2593    }
2594
2595    /// Set the value used to filter streams whose names are lexicographically greater than this
2596    /// value.
2597    pub fn with_start_after(self, start_after: StreamNameStartAfter) -> Self {
2598        Self {
2599            start_after,
2600            ..self
2601        }
2602    }
2603
2604    /// Set the limit on number of streams to return in a page.
2605    pub fn with_limit(self, limit: usize) -> Self {
2606        Self {
2607            limit: Some(limit),
2608            ..self
2609        }
2610    }
2611}
2612
2613impl From<ListStreamsInput> for api::stream::ListStreamsRequest {
2614    fn from(value: ListStreamsInput) -> Self {
2615        Self {
2616            prefix: Some(value.prefix),
2617            start_after: Some(value.start_after),
2618            limit: value.limit,
2619        }
2620    }
2621}
2622
2623#[derive(Debug, Clone, Default)]
2624/// Input for [`list_all_streams`](crate::S2Basin::list_all_streams) operation.
2625pub struct ListAllStreamsInput {
2626    /// Filter streams whose names begin with this value.
2627    ///
2628    /// Defaults to `""`.
2629    pub prefix: StreamNamePrefix,
2630    /// Filter streams whose names are lexicographically greater than this value.
2631    ///
2632    /// Defaults to `""`.
2633    pub start_after: StreamNameStartAfter,
2634    /// Whether to include streams that are being deleted.
2635    ///
2636    /// Defaults to `false`.
2637    pub include_deleted: bool,
2638}
2639
2640impl ListAllStreamsInput {
2641    /// Create a new [`ListAllStreamsInput`] with default values.
2642    pub fn new() -> Self {
2643        Self::default()
2644    }
2645
2646    /// Set the prefix used to filter streams whose names begin with this value.
2647    pub fn with_prefix(self, prefix: StreamNamePrefix) -> Self {
2648        Self { prefix, ..self }
2649    }
2650
2651    /// Set the value used to filter streams whose names are lexicographically greater than this
2652    /// value.
2653    pub fn with_start_after(self, start_after: StreamNameStartAfter) -> Self {
2654        Self {
2655            start_after,
2656            ..self
2657        }
2658    }
2659
2660    /// Set whether to include streams that are being deleted.
2661    pub fn with_include_deleted(self, include_deleted: bool) -> Self {
2662        Self {
2663            include_deleted,
2664            ..self
2665        }
2666    }
2667}
2668
2669#[derive(Debug, Clone, PartialEq, Eq)]
2670#[non_exhaustive]
2671/// Stream information.
2672pub struct StreamInfo {
2673    /// Stream name.
2674    pub name: StreamName,
2675    /// Creation time.
2676    pub created_at: S2DateTime,
2677    /// Deletion time if the stream is being deleted.
2678    pub deleted_at: Option<S2DateTime>,
2679    /// Encryption algorithm for this stream, if encryption is enabled.
2680    pub cipher: Option<EncryptionAlgorithm>,
2681}
2682
2683impl TryFrom<api::stream::StreamInfo> for StreamInfo {
2684    type Error = ValidationError;
2685
2686    fn try_from(value: api::stream::StreamInfo) -> Result<Self, Self::Error> {
2687        Ok(Self {
2688            name: value.name,
2689            created_at: value.created_at.try_into()?,
2690            deleted_at: value.deleted_at.map(S2DateTime::try_from).transpose()?,
2691            cipher: value.cipher.map(Into::into),
2692        })
2693    }
2694}
2695
2696#[derive(Debug, Clone)]
2697#[non_exhaustive]
2698/// Input for [`create_stream`](crate::S2Basin::create_stream) operation.
2699pub struct CreateStreamInput {
2700    /// Stream name.
2701    pub name: StreamName,
2702    /// Configuration for the stream.
2703    ///
2704    /// See [`StreamConfig`] for defaults.
2705    pub config: Option<StreamConfig>,
2706    idempotency_token: String,
2707}
2708
2709impl CreateStreamInput {
2710    /// Create a new [`CreateStreamInput`] with the given stream name.
2711    pub fn new(name: StreamName) -> Self {
2712        Self {
2713            name,
2714            config: None,
2715            idempotency_token: idempotency_token(),
2716        }
2717    }
2718
2719    /// Set the configuration for the stream.
2720    pub fn with_config(self, config: StreamConfig) -> Self {
2721        Self {
2722            config: Some(config),
2723            ..self
2724        }
2725    }
2726}
2727
2728impl From<CreateStreamInput> for (api::stream::CreateStreamRequest, String) {
2729    fn from(value: CreateStreamInput) -> Self {
2730        (
2731            api::stream::CreateStreamRequest {
2732                stream: value.name,
2733                config: value.config.map(Into::into),
2734            },
2735            value.idempotency_token,
2736        )
2737    }
2738}
2739
2740#[derive(Debug, Clone)]
2741#[non_exhaustive]
2742/// Input for [`ensure_stream`](crate::S2Basin::ensure_stream)
2743/// operation.
2744pub struct EnsureStreamInput {
2745    /// Stream name.
2746    pub name: StreamName,
2747    /// Configuration for the stream.
2748    ///
2749    /// See [`StreamConfig`] for defaults.
2750    pub config: Option<StreamConfig>,
2751}
2752
2753impl EnsureStreamInput {
2754    /// Create a new [`EnsureStreamInput`] with the given stream name.
2755    pub fn new(name: StreamName) -> Self {
2756        Self { name, config: None }
2757    }
2758
2759    /// Set the configuration for the stream.
2760    pub fn with_config(self, config: StreamConfig) -> Self {
2761        Self {
2762            config: Some(config),
2763            ..self
2764        }
2765    }
2766}
2767
2768impl From<EnsureStreamInput> for (StreamName, Option<api::config::StreamConfig>) {
2769    fn from(value: EnsureStreamInput) -> Self {
2770        (value.name, value.config.map(Into::into))
2771    }
2772}
2773
2774#[derive(Debug, Clone)]
2775#[non_exhaustive]
2776/// Input of [`delete_stream`](crate::S2Basin::delete_stream) operation.
2777pub struct DeleteStreamInput {
2778    /// Stream name.
2779    pub name: StreamName,
2780    /// Whether to ignore `Not Found` error if the stream doesn't exist.
2781    pub ignore_not_found: bool,
2782}
2783
2784impl DeleteStreamInput {
2785    /// Create a new [`DeleteStreamInput`] with the given stream name.
2786    pub fn new(name: StreamName) -> Self {
2787        Self {
2788            name,
2789            ignore_not_found: false,
2790        }
2791    }
2792
2793    /// Set whether to ignore `Not Found` error if the stream doesn't exist.
2794    pub fn with_ignore_not_found(self, ignore_not_found: bool) -> Self {
2795        Self {
2796            ignore_not_found,
2797            ..self
2798        }
2799    }
2800}
2801
2802#[derive(Debug, Clone)]
2803#[non_exhaustive]
2804/// Input for [`reconfigure_stream`](crate::S2Basin::reconfigure_stream) operation.
2805pub struct ReconfigureStreamInput {
2806    /// Stream name.
2807    pub name: StreamName,
2808    /// Reconfiguration for [`StreamConfig`].
2809    pub config: StreamReconfiguration,
2810}
2811
2812impl ReconfigureStreamInput {
2813    /// Create a new [`ReconfigureStreamInput`] with the given stream name and reconfiguration.
2814    pub fn new(name: StreamName, config: StreamReconfiguration) -> Self {
2815        Self { name, config }
2816    }
2817}
2818
2819#[derive(Debug, Clone, PartialEq, Eq)]
2820/// Token for fencing appends to a stream.
2821///
2822/// **Note:** It must not exceed 36 bytes in length.
2823///
2824/// See [`CommandRecord::fence`] and [`AppendInput::fencing_token`].
2825pub struct FencingToken(String);
2826
2827impl FencingToken {
2828    /// Generate a random alphanumeric fencing token of `n` bytes.
2829    pub fn generate(n: usize) -> Result<Self, ValidationError> {
2830        rand::rng()
2831            .sample_iter(&rand::distr::Alphanumeric)
2832            .take(n)
2833            .map(char::from)
2834            .collect::<String>()
2835            .parse()
2836    }
2837}
2838
2839impl FromStr for FencingToken {
2840    type Err = ValidationError;
2841
2842    fn from_str(s: &str) -> Result<Self, Self::Err> {
2843        if s.len() > MAX_FENCING_TOKEN_LENGTH {
2844            return Err(ValidationError(format!(
2845                "fencing token exceeds {MAX_FENCING_TOKEN_LENGTH} bytes in length",
2846            )));
2847        }
2848        Ok(FencingToken(s.to_string()))
2849    }
2850}
2851
2852impl std::fmt::Display for FencingToken {
2853    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
2854        write!(f, "{}", self.0)
2855    }
2856}
2857
2858impl Deref for FencingToken {
2859    type Target = str;
2860
2861    fn deref(&self) -> &Self::Target {
2862        &self.0
2863    }
2864}
2865
2866#[derive(Debug, Clone, Copy, PartialEq)]
2867#[non_exhaustive]
2868/// A position in a stream.
2869pub struct StreamPosition {
2870    /// Sequence number assigned by the service.
2871    pub seq_num: u64,
2872    /// Timestamp. When assigned by the service, represents milliseconds since Unix epoch.
2873    /// User-specified timestamps are passed through as-is.
2874    pub timestamp: u64,
2875}
2876
2877impl std::fmt::Display for StreamPosition {
2878    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
2879        write!(f, "seq_num={}, timestamp={}", self.seq_num, self.timestamp)
2880    }
2881}
2882
2883impl From<api::stream::proto::StreamPosition> for StreamPosition {
2884    fn from(value: api::stream::proto::StreamPosition) -> Self {
2885        Self {
2886            seq_num: value.seq_num,
2887            timestamp: value.timestamp,
2888        }
2889    }
2890}
2891
2892impl From<api::stream::StreamPosition> for StreamPosition {
2893    fn from(value: api::stream::StreamPosition) -> Self {
2894        Self {
2895            seq_num: value.seq_num,
2896            timestamp: value.timestamp,
2897        }
2898    }
2899}
2900
2901#[derive(Debug, Clone, PartialEq)]
2902#[non_exhaustive]
2903/// A name-value pair.
2904pub struct Header {
2905    /// Name.
2906    pub name: Bytes,
2907    /// Value.
2908    pub value: Bytes,
2909}
2910
2911impl Header {
2912    /// Create a new [`Header`] with the given name and value.
2913    pub fn new(name: impl Into<Bytes>, value: impl Into<Bytes>) -> Self {
2914        Self {
2915            name: name.into(),
2916            value: value.into(),
2917        }
2918    }
2919}
2920
2921impl From<Header> for api::stream::proto::Header {
2922    fn from(value: Header) -> Self {
2923        Self {
2924            name: value.name,
2925            value: value.value,
2926        }
2927    }
2928}
2929
2930impl From<api::stream::proto::Header> for Header {
2931    fn from(value: api::stream::proto::Header) -> Self {
2932        Self {
2933            name: value.name,
2934            value: value.value,
2935        }
2936    }
2937}
2938
2939#[derive(Debug, Clone, PartialEq)]
2940/// A record to append.
2941pub struct AppendRecord {
2942    body: Bytes,
2943    headers: Vec<Header>,
2944    timestamp: Option<u64>,
2945}
2946
2947impl AppendRecord {
2948    fn validate(self) -> Result<Self, ValidationError> {
2949        if self.metered_bytes() > RECORD_BATCH_MAX.bytes {
2950            Err(ValidationError(format!(
2951                "metered_bytes: {} exceeds {}",
2952                self.metered_bytes(),
2953                RECORD_BATCH_MAX.bytes
2954            )))
2955        } else {
2956            Ok(self)
2957        }
2958    }
2959
2960    /// Create a new [`AppendRecord`] with the given record body.
2961    pub fn new(body: impl Into<Bytes>) -> Result<Self, ValidationError> {
2962        let record = Self {
2963            body: body.into(),
2964            headers: Vec::default(),
2965            timestamp: None,
2966        };
2967        record.validate()
2968    }
2969
2970    /// Set the headers for this record.
2971    pub fn with_headers(
2972        self,
2973        headers: impl IntoIterator<Item = Header>,
2974    ) -> Result<Self, ValidationError> {
2975        let record = Self {
2976            headers: headers.into_iter().collect(),
2977            ..self
2978        };
2979        record.validate()
2980    }
2981
2982    /// Set the timestamp for this record.
2983    ///
2984    /// Precise semantics depend on [`StreamConfig::timestamping`].
2985    pub fn with_timestamp(self, timestamp: u64) -> Self {
2986        Self {
2987            timestamp: Some(timestamp),
2988            ..self
2989        }
2990    }
2991
2992    /// Get the body of this record.
2993    pub fn body(&self) -> &[u8] {
2994        &self.body
2995    }
2996
2997    /// Get the headers of this record.
2998    pub fn headers(&self) -> &[Header] {
2999        &self.headers
3000    }
3001
3002    /// Get the timestamp of this record.
3003    pub fn timestamp(&self) -> Option<u64> {
3004        self.timestamp
3005    }
3006}
3007
3008impl From<AppendRecord> for api::stream::proto::AppendRecord {
3009    fn from(value: AppendRecord) -> Self {
3010        Self {
3011            timestamp: value.timestamp,
3012            headers: value.headers.into_iter().map(Into::into).collect(),
3013            body: value.body,
3014        }
3015    }
3016}
3017
3018/// Metered byte size calculation.
3019///
3020/// Formula for a record:
3021/// ```text
3022/// 8 + 2 * len(headers) + sum(len(h.name) + len(h.value) for h in headers) + len(body)
3023/// ```
3024pub trait MeteredBytes {
3025    /// Returns the metered byte size.
3026    fn metered_bytes(&self) -> usize;
3027}
3028
3029macro_rules! metered_bytes_impl {
3030    ($ty:ty) => {
3031        impl MeteredBytes for $ty {
3032            fn metered_bytes(&self) -> usize {
3033                8 + (2 * self.headers.len())
3034                    + self
3035                        .headers
3036                        .iter()
3037                        .map(|h| h.name.len() + h.value.len())
3038                        .sum::<usize>()
3039                    + self.body.len()
3040            }
3041        }
3042    };
3043}
3044
3045metered_bytes_impl!(AppendRecord);
3046
3047#[derive(Debug, Clone)]
3048/// A batch of records to append atomically.
3049///
3050/// **Note:** It must contain at least `1` record and no more than `1000`.
3051/// The total size of the batch must not exceed `1MiB` in metered bytes.
3052///
3053/// See [`AppendRecordBatches`](crate::batching::AppendRecordBatches) and
3054/// [`AppendInputs`](crate::batching::AppendInputs) for convenient and automatic batching of records
3055/// that takes care of the abovementioned constraints.
3056pub struct AppendRecordBatch {
3057    records: Vec<AppendRecord>,
3058    metered_bytes: usize,
3059}
3060
3061impl AppendRecordBatch {
3062    pub(crate) fn with_capacity(capacity: usize) -> Self {
3063        Self {
3064            records: Vec::with_capacity(capacity),
3065            metered_bytes: 0,
3066        }
3067    }
3068
3069    pub(crate) fn push(&mut self, record: AppendRecord) {
3070        self.metered_bytes += record.metered_bytes();
3071        self.records.push(record);
3072    }
3073
3074    /// Try to create an [`AppendRecordBatch`] from an iterator of [`AppendRecord`]s.
3075    pub fn try_from_iter<I>(iter: I) -> Result<Self, ValidationError>
3076    where
3077        I: IntoIterator<Item = AppendRecord>,
3078    {
3079        let mut records = Vec::new();
3080        let mut metered_bytes = 0;
3081
3082        for record in iter {
3083            metered_bytes += record.metered_bytes();
3084            records.push(record);
3085
3086            if metered_bytes > RECORD_BATCH_MAX.bytes {
3087                return Err(ValidationError(format!(
3088                    "batch size in metered bytes ({metered_bytes}) exceeds {}",
3089                    RECORD_BATCH_MAX.bytes
3090                )));
3091            }
3092
3093            if records.len() > RECORD_BATCH_MAX.count {
3094                return Err(ValidationError(format!(
3095                    "number of records in the batch exceeds {}",
3096                    RECORD_BATCH_MAX.count
3097                )));
3098            }
3099        }
3100
3101        if records.is_empty() {
3102            return Err(ValidationError("batch is empty".into()));
3103        }
3104
3105        Ok(Self {
3106            records,
3107            metered_bytes,
3108        })
3109    }
3110}
3111
3112impl Deref for AppendRecordBatch {
3113    type Target = [AppendRecord];
3114
3115    fn deref(&self) -> &Self::Target {
3116        &self.records
3117    }
3118}
3119
3120impl MeteredBytes for AppendRecordBatch {
3121    fn metered_bytes(&self) -> usize {
3122        self.metered_bytes
3123    }
3124}
3125
3126#[derive(Debug, Clone)]
3127/// Command to signal an operation.
3128pub enum Command {
3129    /// Fence operation.
3130    Fence {
3131        /// Fencing token.
3132        fencing_token: FencingToken,
3133    },
3134    /// Trim operation.
3135    Trim {
3136        /// Trim point.
3137        trim_point: u64,
3138    },
3139}
3140
3141#[derive(Debug, Clone)]
3142#[non_exhaustive]
3143/// Command record for signaling operations to the service.
3144///
3145/// See [here](https://s2.dev/docs/rest/records/overview#command-records) for more information.
3146pub struct CommandRecord {
3147    /// Command to signal an operation.
3148    pub command: Command,
3149    /// Timestamp for this record.
3150    pub timestamp: Option<u64>,
3151}
3152
3153impl CommandRecord {
3154    const FENCE: &[u8] = b"fence";
3155    const TRIM: &[u8] = b"trim";
3156
3157    /// Create a fence command record with the given fencing token.
3158    ///
3159    /// Fencing is strongly consistent, and subsequent appends that specify a
3160    /// fencing token will fail if it does not match.
3161    pub fn fence(fencing_token: FencingToken) -> Self {
3162        Self {
3163            command: Command::Fence { fencing_token },
3164            timestamp: None,
3165        }
3166    }
3167
3168    /// Create a trim command record with the given trim point.
3169    ///
3170    /// Trim point is the desired earliest sequence number for the stream.
3171    ///
3172    /// Trimming is eventually consistent, and trimmed records may be visible
3173    /// for a brief period.
3174    pub fn trim(trim_point: u64) -> Self {
3175        Self {
3176            command: Command::Trim { trim_point },
3177            timestamp: None,
3178        }
3179    }
3180
3181    /// Set the timestamp for this record.
3182    pub fn with_timestamp(self, timestamp: u64) -> Self {
3183        Self {
3184            timestamp: Some(timestamp),
3185            ..self
3186        }
3187    }
3188}
3189
3190impl From<CommandRecord> for AppendRecord {
3191    fn from(value: CommandRecord) -> Self {
3192        let (header_value, body) = match value.command {
3193            Command::Fence { fencing_token } => (
3194                CommandRecord::FENCE,
3195                Bytes::copy_from_slice(fencing_token.as_bytes()),
3196            ),
3197            Command::Trim { trim_point } => (
3198                CommandRecord::TRIM,
3199                Bytes::copy_from_slice(&trim_point.to_be_bytes()),
3200            ),
3201        };
3202        Self {
3203            body,
3204            headers: vec![Header::new("", header_value)],
3205            timestamp: value.timestamp,
3206        }
3207    }
3208}
3209
3210#[derive(Debug, Clone)]
3211#[non_exhaustive]
3212/// Input for [`append`](crate::S2Stream::append) operation and
3213/// [`AppendSession::submit`](crate::append_session::AppendSession::submit).
3214pub struct AppendInput {
3215    /// Batch of records to append atomically.
3216    pub records: AppendRecordBatch,
3217    /// Expected sequence number for the first record in the batch.
3218    ///
3219    /// If unspecified, no matching is performed. If specified and mismatched, the append fails.
3220    pub match_seq_num: Option<u64>,
3221    /// Fencing token to match against the stream's current fencing token.
3222    ///
3223    /// If unspecified, no matching is performed. If specified and mismatched,
3224    /// the append fails. A stream defaults to `""` as its fencing token.
3225    pub fencing_token: Option<FencingToken>,
3226}
3227
3228impl AppendInput {
3229    /// Create a new [`AppendInput`] with the given batch of records.
3230    pub fn new(records: AppendRecordBatch) -> Self {
3231        Self {
3232            records,
3233            match_seq_num: None,
3234            fencing_token: None,
3235        }
3236    }
3237
3238    /// Set the expected sequence number for the first record in the batch.
3239    pub fn with_match_seq_num(self, match_seq_num: u64) -> Self {
3240        Self {
3241            match_seq_num: Some(match_seq_num),
3242            ..self
3243        }
3244    }
3245
3246    /// Set the fencing token to match against the stream's current fencing token.
3247    pub fn with_fencing_token(self, fencing_token: FencingToken) -> Self {
3248        Self {
3249            fencing_token: Some(fencing_token),
3250            ..self
3251        }
3252    }
3253}
3254
3255impl From<AppendInput> for api::stream::proto::AppendInput {
3256    fn from(value: AppendInput) -> Self {
3257        Self {
3258            records: value.records.iter().cloned().map(Into::into).collect(),
3259            match_seq_num: value.match_seq_num,
3260            fencing_token: value.fencing_token.map(|t| t.to_string()),
3261        }
3262    }
3263}
3264
3265#[derive(Debug, Clone, PartialEq)]
3266#[non_exhaustive]
3267/// Acknowledgement for an [`AppendInput`].
3268pub struct AppendAck {
3269    /// Sequence number and timestamp of the first record that was appended.
3270    pub start: StreamPosition,
3271    /// Sequence number of the last record that was appended + 1, and timestamp of the last record
3272    /// that was appended.
3273    ///
3274    /// The difference between `end.seq_num` and `start.seq_num` will be the number of records
3275    /// appended.
3276    pub end: StreamPosition,
3277    /// Sequence number that will be assigned to the next record on the stream, and timestamp of
3278    /// the last record on the stream.
3279    ///
3280    /// This can be greater than the `end` position in case of concurrent appends.
3281    pub tail: StreamPosition,
3282}
3283
3284impl From<api::stream::proto::AppendAck> for AppendAck {
3285    fn from(value: api::stream::proto::AppendAck) -> Self {
3286        Self {
3287            start: value.start.unwrap_or_default().into(),
3288            end: value.end.unwrap_or_default().into(),
3289            tail: value.tail.unwrap_or_default().into(),
3290        }
3291    }
3292}
3293
3294#[derive(Debug, Clone, Copy)]
3295/// Starting position for reading from a stream.
3296pub enum ReadFrom {
3297    /// Read from this sequence number.
3298    SeqNum(u64),
3299    /// Read from this timestamp.
3300    Timestamp(u64),
3301    /// Read from N records before the tail.
3302    TailOffset(u64),
3303}
3304
3305impl Default for ReadFrom {
3306    fn default() -> Self {
3307        Self::SeqNum(0)
3308    }
3309}
3310
3311#[derive(Debug, Default, Clone)]
3312#[non_exhaustive]
3313/// Where to start reading.
3314pub struct ReadStart {
3315    /// Starting position.
3316    ///
3317    /// Defaults to reading from sequence number `0`.
3318    pub from: ReadFrom,
3319    /// Whether to start from tail if the requested starting position is beyond it.
3320    ///
3321    /// Defaults to `false` (errors if position is beyond tail).
3322    pub clamp_to_tail: bool,
3323}
3324
3325impl ReadStart {
3326    /// Create a new [`ReadStart`] with default values.
3327    pub fn new() -> Self {
3328        Self::default()
3329    }
3330
3331    /// Set the starting position.
3332    pub fn with_from(self, from: ReadFrom) -> Self {
3333        Self { from, ..self }
3334    }
3335
3336    /// Set whether to start from tail if the requested starting position is beyond it.
3337    pub fn with_clamp_to_tail(self, clamp_to_tail: bool) -> Self {
3338        Self {
3339            clamp_to_tail,
3340            ..self
3341        }
3342    }
3343}
3344
3345impl From<ReadStart> for api::stream::ReadStart {
3346    fn from(value: ReadStart) -> Self {
3347        let (seq_num, timestamp, tail_offset) = match value.from {
3348            ReadFrom::SeqNum(n) => (Some(n), None, None),
3349            ReadFrom::Timestamp(t) => (None, Some(t), None),
3350            ReadFrom::TailOffset(o) => (None, None, Some(o)),
3351        };
3352        Self {
3353            seq_num,
3354            timestamp,
3355            tail_offset,
3356            clamp: if value.clamp_to_tail {
3357                Some(true)
3358            } else {
3359                None
3360            },
3361        }
3362    }
3363}
3364
3365#[derive(Debug, Clone, Default)]
3366#[non_exhaustive]
3367/// Limits on how much to read.
3368pub struct ReadLimits {
3369    /// Limit on number of records.
3370    ///
3371    /// Defaults to `1000` for non-streaming read.
3372    pub count: Option<usize>,
3373    /// Limit on total metered bytes of records.
3374    ///
3375    /// Defaults to `1MiB` for non-streaming read.
3376    pub bytes: Option<usize>,
3377}
3378
3379impl ReadLimits {
3380    /// Create a new [`ReadLimits`] with default values.
3381    pub fn new() -> Self {
3382        Self::default()
3383    }
3384
3385    /// Set the limit on number of records.
3386    pub fn with_count(self, count: usize) -> Self {
3387        Self {
3388            count: Some(count),
3389            ..self
3390        }
3391    }
3392
3393    /// Set the limit on total metered bytes of records.
3394    pub fn with_bytes(self, bytes: usize) -> Self {
3395        Self {
3396            bytes: Some(bytes),
3397            ..self
3398        }
3399    }
3400}
3401
3402#[derive(Debug, Clone, Default)]
3403#[non_exhaustive]
3404/// When to stop reading.
3405pub struct ReadStop {
3406    /// Limits on how much to read.
3407    ///
3408    /// See [`ReadLimits`] for defaults.
3409    pub limits: ReadLimits,
3410    /// Timestamp at which to stop (exclusive).
3411    ///
3412    /// Defaults to `None`.
3413    pub until: Option<RangeTo<u64>>,
3414    /// Duration in seconds to wait for new records before stopping. Will be clamped to `60`
3415    /// seconds for [`read`](crate::S2Stream::read).
3416    ///
3417    /// Defaults to:
3418    /// - `0` (no wait) for [`read`](crate::S2Stream::read).
3419    /// - `0` (no wait) for [`read_session`](crate::S2Stream::read_session) if `limits` or `until`
3420    ///   is specified.
3421    /// - Infinite wait for [`read_session`](crate::S2Stream::read_session) if neither `limits` nor
3422    ///   `until` is specified.
3423    pub wait: Option<u32>,
3424}
3425
3426impl ReadStop {
3427    /// Create a new [`ReadStop`] with default values.
3428    pub fn new() -> Self {
3429        Self::default()
3430    }
3431
3432    /// Set the limits on how much to read.
3433    pub fn with_limits(self, limits: ReadLimits) -> Self {
3434        Self { limits, ..self }
3435    }
3436
3437    /// Set the timestamp at which to stop (exclusive).
3438    pub fn with_until(self, until: RangeTo<u64>) -> Self {
3439        Self {
3440            until: Some(until),
3441            ..self
3442        }
3443    }
3444
3445    /// Set the duration in seconds to wait for new records before stopping.
3446    pub fn with_wait(self, wait: u32) -> Self {
3447        Self {
3448            wait: Some(wait),
3449            ..self
3450        }
3451    }
3452}
3453
3454impl From<ReadStop> for api::stream::ReadEnd {
3455    fn from(value: ReadStop) -> Self {
3456        Self {
3457            count: value.limits.count,
3458            bytes: value.limits.bytes,
3459            until: value.until.map(|r| r.end),
3460            wait: value.wait,
3461        }
3462    }
3463}
3464
3465#[derive(Debug, Clone, Default)]
3466#[non_exhaustive]
3467/// Input for [`read`](crate::S2Stream::read) and [`read_session`](crate::S2Stream::read_session)
3468/// operations.
3469pub struct ReadInput {
3470    /// Where to start reading.
3471    ///
3472    /// See [`ReadStart`] for defaults.
3473    pub start: ReadStart,
3474    /// When to stop reading.
3475    ///
3476    /// See [`ReadStop`] for defaults.
3477    pub stop: ReadStop,
3478    /// Whether to filter out command records from the stream when reading.
3479    ///
3480    /// Defaults to `false`.
3481    pub ignore_command_records: bool,
3482}
3483
3484impl ReadInput {
3485    /// Create a new [`ReadInput`] with default values.
3486    pub fn new() -> Self {
3487        Self::default()
3488    }
3489
3490    /// Set where to start reading.
3491    pub fn with_start(self, start: ReadStart) -> Self {
3492        Self { start, ..self }
3493    }
3494
3495    /// Set when to stop reading.
3496    pub fn with_stop(self, stop: ReadStop) -> Self {
3497        Self { stop, ..self }
3498    }
3499
3500    /// Set whether to filter out command records from the stream when reading.
3501    pub fn with_ignore_command_records(self, ignore_command_records: bool) -> Self {
3502        Self {
3503            ignore_command_records,
3504            ..self
3505        }
3506    }
3507}
3508
3509#[derive(Debug, Clone)]
3510#[non_exhaustive]
3511/// Record that is durably sequenced on a stream.
3512pub struct SequencedRecord {
3513    /// Sequence number assigned to this record.
3514    pub seq_num: u64,
3515    /// Body of this record.
3516    pub body: Bytes,
3517    /// Headers for this record.
3518    pub headers: Vec<Header>,
3519    /// Timestamp for this record.
3520    pub timestamp: u64,
3521}
3522
3523impl SequencedRecord {
3524    #[doc(hidden)]
3525    #[cfg(feature = "_hidden")]
3526    pub fn from_parts(
3527        seq_num: u64,
3528        timestamp: u64,
3529        headers: Vec<Header>,
3530        body: impl Into<Bytes>,
3531    ) -> Self {
3532        Self {
3533            seq_num,
3534            timestamp,
3535            body: body.into(),
3536            headers,
3537        }
3538    }
3539
3540    /// Whether this is a command record.
3541    pub fn is_command_record(&self) -> bool {
3542        self.headers.len() == 1 && *self.headers[0].name == *b""
3543    }
3544}
3545
3546impl From<api::stream::proto::SequencedRecord> for SequencedRecord {
3547    fn from(value: api::stream::proto::SequencedRecord) -> Self {
3548        Self {
3549            seq_num: value.seq_num,
3550            body: value.body,
3551            headers: value.headers.into_iter().map(Into::into).collect(),
3552            timestamp: value.timestamp,
3553        }
3554    }
3555}
3556
3557metered_bytes_impl!(SequencedRecord);
3558
3559#[derive(Debug, Clone)]
3560#[non_exhaustive]
3561/// Batch of records returned by [`read`](crate::S2Stream::read) or streamed by
3562/// [`read_session`](crate::S2Stream::read_session).
3563pub struct ReadBatch {
3564    /// Records that are durably sequenced on the stream.
3565    ///
3566    /// It can be empty only for a [`read`](crate::S2Stream::read) operation when:
3567    /// - the [`stop condition`](ReadInput::stop) was already met, or
3568    /// - all records in the batch were command records and
3569    ///   [`ignore_command_records`](ReadInput::ignore_command_records) was set to `true`.
3570    pub records: Vec<SequencedRecord>,
3571    /// Sequence number that will be assigned to the next record on the stream, and timestamp of
3572    /// the last record.
3573    ///
3574    /// It will only be present when reading recent records.
3575    pub tail: Option<StreamPosition>,
3576}
3577
3578impl ReadBatch {
3579    pub(crate) fn from_api(batch: api::stream::proto::ReadBatch) -> Self {
3580        Self {
3581            records: batch.records.into_iter().map(Into::into).collect(),
3582            tail: batch.tail.map(Into::into),
3583        }
3584    }
3585}
3586
3587/// A [`Stream`](futures_core::Stream) of values of type `Result<T, S2Error>`.
3588pub type Streaming<T> = Pin<Box<dyn Send + futures_core::Stream<Item = Result<T, S2Error>>>>;
3589
3590#[derive(Debug, Clone, thiserror::Error)]
3591/// Why an append condition check failed.
3592pub enum AppendConditionFailed {
3593    #[error("fencing token mismatch, expected: {0}")]
3594    /// Fencing token did not match. Contains the expected fencing token.
3595    FencingTokenMismatch(FencingToken),
3596    #[error("sequence number mismatch, expected: {0}")]
3597    /// Sequence number did not match. Contains the expected sequence number.
3598    SeqNumMismatch(u64),
3599}
3600
3601impl From<api::stream::AppendConditionFailed> for AppendConditionFailed {
3602    fn from(value: api::stream::AppendConditionFailed) -> Self {
3603        match value {
3604            api::stream::AppendConditionFailed::FencingTokenMismatch(token) => {
3605                AppendConditionFailed::FencingTokenMismatch(FencingToken(token.to_string()))
3606            }
3607            api::stream::AppendConditionFailed::SeqNumMismatch(seq) => {
3608                AppendConditionFailed::SeqNumMismatch(seq)
3609            }
3610        }
3611    }
3612}
3613
3614#[derive(Debug, Clone, thiserror::Error)]
3615/// Errors from S2 operations.
3616pub enum S2Error {
3617    #[error("{0}")]
3618    /// Client-side error.
3619    Client(String),
3620    #[error("malformed access token: {0}")]
3621    /// Access token could not be used as an HTTP header value.
3622    MalformedAccessToken(String),
3623    #[error(transparent)]
3624    /// Validation error.
3625    Validation(#[from] ValidationError),
3626    #[error("{0}")]
3627    /// Append condition check failed. Contains the failure reason.
3628    AppendConditionFailed(AppendConditionFailed),
3629    #[error("read from an unwritten position. current tail: {0}")]
3630    /// Read from an unwritten position. Contains the current tail.
3631    ReadUnwritten(StreamPosition),
3632    #[error("{0}")]
3633    /// Other server-side error.
3634    Server(ErrorResponse),
3635}
3636
3637impl From<ApiError> for S2Error {
3638    fn from(err: ApiError) -> Self {
3639        match err {
3640            ApiError::ReadUnwritten(tail_response) => {
3641                Self::ReadUnwritten(tail_response.tail.into())
3642            }
3643            ApiError::AppendConditionFailed(condition_failed) => {
3644                Self::AppendConditionFailed(condition_failed.into())
3645            }
3646            ApiError::Server(_, response) => Self::Server(response.into()),
3647            ApiError::MalformedAccessToken(err) => Self::MalformedAccessToken(err),
3648            other => Self::Client(other.to_string()),
3649        }
3650    }
3651}
3652
3653#[derive(Debug, Clone, thiserror::Error)]
3654#[error("{code}: {message}")]
3655#[non_exhaustive]
3656/// Error response from S2 server.
3657pub struct ErrorResponse {
3658    /// Error code.
3659    pub code: String,
3660    /// Error message.
3661    pub message: String,
3662}
3663
3664impl From<ApiErrorResponse> for ErrorResponse {
3665    fn from(response: ApiErrorResponse) -> Self {
3666        Self {
3667            code: response.code,
3668            message: response.message,
3669        }
3670    }
3671}
3672
3673fn idempotency_token() -> String {
3674    uuid::Uuid::new_v4().simple().to_string()
3675}
3676
3677#[cfg(test)]
3678mod tests {
3679    use proptest::prelude::*;
3680    use rstest::rstest;
3681
3682    use super::*;
3683    use crate::api::ClientError;
3684
3685    type HeaderParts = (Vec<u8>, Vec<u8>);
3686    type AppendRecordParts = (Vec<u8>, Vec<HeaderParts>);
3687
3688    fn byte_vec_strategy(max_len: usize) -> impl Strategy<Value = Vec<u8>> {
3689        prop::collection::vec(any::<u8>(), 0..=max_len)
3690    }
3691
3692    fn header_parts_strategy() -> impl Strategy<Value = HeaderParts> {
3693        (byte_vec_strategy(32), byte_vec_strategy(64))
3694    }
3695
3696    fn string_strategy(max_chars: usize) -> impl Strategy<Value = String> {
3697        prop::collection::vec(any::<char>(), 0..=max_chars)
3698            .prop_map(|chars| chars.into_iter().collect())
3699    }
3700
3701    fn read_from_strategy() -> impl Strategy<Value = ReadFrom> {
3702        prop_oneof![
3703            any::<u64>().prop_map(ReadFrom::SeqNum),
3704            any::<u64>().prop_map(ReadFrom::Timestamp),
3705            any::<u64>().prop_map(ReadFrom::TailOffset),
3706        ]
3707    }
3708
3709    fn append_record_parts_strategy() -> impl Strategy<Value = AppendRecordParts> {
3710        (
3711            byte_vec_strategy(256),
3712            prop::collection::vec(header_parts_strategy(), 0..=16),
3713        )
3714    }
3715
3716    fn proto_stream_position_strategy() -> impl Strategy<Value = api::stream::proto::StreamPosition>
3717    {
3718        (any::<u64>(), any::<u64>()).prop_map(|(seq_num, timestamp)| {
3719            api::stream::proto::StreamPosition { seq_num, timestamp }
3720        })
3721    }
3722
3723    fn headers_from_parts(headers: &[HeaderParts]) -> Vec<Header> {
3724        headers
3725            .iter()
3726            .map(|(name, value)| Header::new(Bytes::from(name.clone()), Bytes::from(value.clone())))
3727            .collect()
3728    }
3729
3730    fn expected_metered_bytes(body: &[u8], headers: &[HeaderParts]) -> usize {
3731        8 + (2 * headers.len())
3732            + headers
3733                .iter()
3734                .map(|(name, value)| name.len() + value.len())
3735                .sum::<usize>()
3736            + body.len()
3737    }
3738
3739    // -- S2DateTime --
3740
3741    #[test]
3742    fn s2_datetime_parse_valid_rfc3339() {
3743        let dt: S2DateTime = "2024-01-15T12:30:00Z".parse().unwrap();
3744        assert_eq!(dt.to_string(), "2024-01-15T12:30:00Z");
3745    }
3746
3747    #[test]
3748    fn s2_datetime_parse_with_offset() {
3749        let dt: S2DateTime = "2024-06-01T08:00:00+05:30".parse().unwrap();
3750        assert_eq!(dt.to_string(), "2024-06-01T08:00:00+05:30");
3751
3752        let offset_dt: time::OffsetDateTime = dt.into();
3753        assert_eq!(
3754            offset_dt.offset(),
3755            time::UtcOffset::from_hms(5, 30, 0).unwrap()
3756        );
3757    }
3758
3759    #[test]
3760    fn s2_datetime_parse_invalid() {
3761        let err = "not-a-date".parse::<S2DateTime>();
3762        assert!(err.is_err());
3763    }
3764
3765    #[test]
3766    fn s2_datetime_roundtrip_via_offset_datetime() {
3767        let odt = time::OffsetDateTime::from_unix_timestamp(1_700_000_000).unwrap();
3768        let dt = S2DateTime::try_from(odt).unwrap();
3769        let back: time::OffsetDateTime = dt.into();
3770        assert_eq!(odt, back);
3771    }
3772
3773    // -- AccountEndpoint --
3774
3775    #[rstest]
3776    #[case::https_with_scheme("https://aws.s2.dev", Scheme::HTTPS)]
3777    #[case::http_with_scheme("http://localhost:8080", Scheme::HTTP)]
3778    #[case::default_https("aws.s2.dev", Scheme::HTTPS)]
3779    fn account_endpoint_parse(#[case] input: &str, #[case] expected_scheme: Scheme) {
3780        let ep: AccountEndpoint = input.parse().unwrap();
3781        assert_eq!(ep.scheme, expected_scheme);
3782    }
3783
3784    // -- BasinEndpoint --
3785
3786    #[rstest]
3787    #[case::https_parent_zone("https://{basin}.b.s2.dev", Scheme::HTTPS, true)]
3788    #[case::http_direct("http://localhost:8080", Scheme::HTTP, false)]
3789    #[case::default_https_parent_zone("{basin}.b.s2.dev", Scheme::HTTPS, true)]
3790    fn basin_endpoint_parse(
3791        #[case] input: &str,
3792        #[case] expected_scheme: Scheme,
3793        #[case] expected_parent_zone: bool,
3794    ) {
3795        let ep: BasinEndpoint = input.parse().unwrap();
3796        assert_eq!(ep.scheme, expected_scheme);
3797        assert_eq!(
3798            matches!(ep.authority, BasinAuthority::ParentZone(_)),
3799            expected_parent_zone
3800        );
3801    }
3802
3803    // -- S2Endpoints --
3804
3805    #[test]
3806    fn s2_endpoints_new_requires_same_scheme() {
3807        let account: AccountEndpoint = "https://aws.s2.dev".parse().unwrap();
3808        let basin: BasinEndpoint = "http://localhost:8080".parse().unwrap();
3809        let err = S2Endpoints::new(account, basin);
3810        assert!(err.is_err());
3811    }
3812
3813    #[test]
3814    fn s2_endpoints_new_same_scheme_succeeds() {
3815        let account: AccountEndpoint = "https://aws.s2.dev".parse().unwrap();
3816        let basin: BasinEndpoint = "https://{basin}.b.s2.dev".parse().unwrap();
3817        let ep = S2Endpoints::new(account, basin).unwrap();
3818        assert_eq!(ep.scheme, Scheme::HTTPS);
3819    }
3820
3821    // -- Compression --
3822
3823    #[rstest]
3824    #[case::none(Compression::None, CompressionAlgorithm::None)]
3825    #[case::gzip(Compression::Gzip, CompressionAlgorithm::Gzip)]
3826    #[case::zstd(Compression::Zstd, CompressionAlgorithm::Zstd)]
3827    fn compression_conversion(#[case] sdk: Compression, #[case] api: CompressionAlgorithm) {
3828        assert_eq!(CompressionAlgorithm::from(sdk), api);
3829    }
3830
3831    // -- RetryConfig --
3832
3833    #[test]
3834    fn retry_config_defaults() {
3835        let rc = RetryConfig::default();
3836        assert_eq!(rc.max_attempts.get(), 3);
3837        assert_eq!(rc.min_base_delay, Duration::from_millis(100));
3838        assert_eq!(rc.max_base_delay, Duration::from_secs(1));
3839        assert!(matches!(rc.append_retry_policy, AppendRetryPolicy::All));
3840    }
3841
3842    #[test]
3843    fn retry_config_max_retries() {
3844        let rc = RetryConfig::default();
3845        assert_eq!(rc.max_retries(), 2);
3846    }
3847
3848    // -- S2Config --
3849
3850    #[test]
3851    fn s2_config_defaults() {
3852        let cfg = S2Config::new("test-token");
3853        assert_eq!(cfg.connection_timeout, Duration::from_secs(3));
3854        assert_eq!(cfg.request_timeout, Duration::from_secs(5));
3855        assert!(!cfg.insecure_skip_cert_verification);
3856    }
3857
3858    // -- StorageClass --
3859
3860    #[rstest]
3861    #[case::standard(StorageClass::Standard)]
3862    #[case::express(StorageClass::Express)]
3863    fn storage_class_roundtrip(#[case] sdk: StorageClass) {
3864        let api: api::config::StorageClass = sdk.into();
3865        let back: StorageClass = api.into();
3866        assert_eq!(back, sdk);
3867    }
3868
3869    // -- RetentionPolicy --
3870
3871    #[rstest]
3872    #[case::age(RetentionPolicy::Age(3600))]
3873    #[case::infinite(RetentionPolicy::Infinite)]
3874    fn retention_policy_roundtrip(#[case] sdk: RetentionPolicy) {
3875        let api: api::config::RetentionPolicy = sdk.into();
3876        let back: RetentionPolicy = api.into();
3877        assert_eq!(back, sdk);
3878    }
3879
3880    // -- TimestampingMode --
3881
3882    #[rstest]
3883    #[case::client_prefer(
3884        TimestampingMode::ClientPrefer,
3885        api::config::TimestampingMode::ClientPrefer
3886    )]
3887    #[case::client_require(
3888        TimestampingMode::ClientRequire,
3889        api::config::TimestampingMode::ClientRequire
3890    )]
3891    #[case::arrival(TimestampingMode::Arrival, api::config::TimestampingMode::Arrival)]
3892    fn timestamping_mode_roundtrip(
3893        #[case] sdk: TimestampingMode,
3894        #[case] expected_api: api::config::TimestampingMode,
3895    ) {
3896        let converted: api::config::TimestampingMode = sdk.into();
3897        assert_eq!(converted, expected_api);
3898        let back: TimestampingMode = converted.into();
3899        assert_eq!(back, sdk);
3900    }
3901
3902    // -- TimestampingConfig --
3903
3904    #[test]
3905    fn timestamping_config_roundtrip() {
3906        let sdk = TimestampingConfig {
3907            mode: Some(TimestampingMode::Arrival),
3908            uncapped: Some(true),
3909        };
3910        let api: api::config::TimestampingConfig = sdk.into();
3911        let back: TimestampingConfig = api.into();
3912        assert_eq!(back, sdk);
3913    }
3914
3915    // -- DeleteOnEmptyConfig --
3916
3917    #[test]
3918    fn delete_on_empty_config_roundtrip() {
3919        let sdk = DeleteOnEmptyConfig::new().with_min_age(Duration::from_secs(300));
3920        let api: api::config::DeleteOnEmptyConfig = sdk.into();
3921        let back: DeleteOnEmptyConfig = api.into();
3922        assert_eq!(back, sdk);
3923    }
3924
3925    // -- StreamConfig --
3926
3927    #[test]
3928    fn stream_config_builder_and_roundtrip() {
3929        let sdk = StreamConfig::new()
3930            .with_storage_class(StorageClass::Express)
3931            .with_retention_policy(RetentionPolicy::Age(86400))
3932            .with_timestamping(TimestampingConfig {
3933                mode: Some(TimestampingMode::ClientPrefer),
3934                uncapped: None,
3935            })
3936            .with_delete_on_empty(DeleteOnEmptyConfig { min_age_secs: 60 });
3937        let api: api::config::StreamConfig = sdk.clone().into();
3938        let back: StreamConfig = api.into();
3939        assert_eq!(back, sdk);
3940    }
3941
3942    // -- BasinConfig --
3943
3944    #[test]
3945    fn basin_config_builder_and_roundtrip() {
3946        let sdk = BasinConfig::new()
3947            .with_default_stream_config(
3948                StreamConfig::new().with_storage_class(StorageClass::Standard),
3949            )
3950            .with_create_stream_on_append(true)
3951            .with_create_stream_on_read(false);
3952        let api: api::config::BasinConfig = sdk.clone().into();
3953        let back: BasinConfig = api.into();
3954        assert_eq!(back, sdk);
3955    }
3956
3957    // -- FencingToken --
3958
3959    proptest! {
3960        #[test]
3961        fn fencing_token_parse_accepts_only_within_byte_limit(
3962            token in string_strategy(MAX_FENCING_TOKEN_LENGTH + 8),
3963        ) {
3964            let parsed = token.parse::<FencingToken>();
3965
3966            if token.len() <= MAX_FENCING_TOKEN_LENGTH {
3967                prop_assert_eq!(parsed.unwrap().to_string(), token);
3968            } else {
3969                prop_assert!(parsed.is_err());
3970            }
3971        }
3972    }
3973
3974    // -- StreamPosition --
3975
3976    #[test]
3977    fn stream_position_display() {
3978        let pos = StreamPosition {
3979            seq_num: 42,
3980            timestamp: 1700000000,
3981        };
3982        assert_eq!(pos.to_string(), "seq_num=42, timestamp=1700000000");
3983    }
3984
3985    proptest! {
3986        #[test]
3987        fn stream_position_conversions_preserve_values(seq_num in any::<u64>(), timestamp in any::<u64>()) {
3988            let proto: StreamPosition = api::stream::proto::StreamPosition {
3989                seq_num,
3990                timestamp,
3991            }
3992            .into();
3993            prop_assert_eq!(proto.seq_num, seq_num);
3994            prop_assert_eq!(proto.timestamp, timestamp);
3995
3996            let api: StreamPosition = api::stream::StreamPosition {
3997                seq_num,
3998                timestamp,
3999            }
4000            .into();
4001            prop_assert_eq!(api.seq_num, seq_num);
4002            prop_assert_eq!(api.timestamp, timestamp);
4003        }
4004    }
4005
4006    // -- Header --
4007
4008    proptest! {
4009        #[test]
4010        fn header_proto_roundtrip_preserves_binary_parts(
4011            name in byte_vec_strategy(64),
4012            value in byte_vec_strategy(128),
4013        ) {
4014            let header = Header::new(Bytes::from(name.clone()), Bytes::from(value.clone()));
4015            let proto: api::stream::proto::Header = header.into();
4016            let back: Header = proto.into();
4017
4018            prop_assert_eq!(back.name.as_ref(), name.as_slice());
4019            prop_assert_eq!(back.value.as_ref(), value.as_slice());
4020        }
4021    }
4022
4023    // -- AppendRecord --
4024
4025    #[test]
4026    fn append_record_too_large() {
4027        let big_body = vec![0u8; RECORD_BATCH_MAX.bytes + 1];
4028        assert!(AppendRecord::new(big_body).is_err());
4029    }
4030
4031    // -- MeteredBytes --
4032
4033    proptest! {
4034        #[test]
4035        fn append_record_preserves_fields_and_metered_byte_formula(
4036            (body, headers) in append_record_parts_strategy(),
4037            timestamp in proptest::option::of(any::<u64>()),
4038        ) {
4039            let mut record = AppendRecord::new(body.clone())
4040                .unwrap()
4041                .with_headers(headers_from_parts(&headers))
4042                .unwrap();
4043            if let Some(timestamp) = timestamp {
4044                record = record.with_timestamp(timestamp);
4045            }
4046
4047            prop_assert_eq!(record.body(), body.as_slice());
4048            prop_assert_eq!(record.headers().len(), headers.len());
4049            prop_assert_eq!(record.timestamp(), timestamp);
4050            prop_assert_eq!(record.metered_bytes(), expected_metered_bytes(&body, &headers));
4051
4052            for (actual, (expected_name, expected_value)) in record.headers().iter().zip(headers.iter()) {
4053                prop_assert_eq!(actual.name.as_ref(), expected_name.as_slice());
4054                prop_assert_eq!(actual.value.as_ref(), expected_value.as_slice());
4055            }
4056        }
4057    }
4058
4059    // -- AppendRecordBatch --
4060
4061    #[test]
4062    fn append_record_batch_empty_is_err() {
4063        let result = AppendRecordBatch::try_from_iter(vec![]);
4064        assert!(result.is_err());
4065    }
4066
4067    #[test]
4068    fn append_record_batch_too_many_records() {
4069        let records: Vec<_> = (0..1001).map(|_| AppendRecord::new("x").unwrap()).collect();
4070        let result = AppendRecordBatch::try_from_iter(records);
4071        assert!(result.is_err());
4072    }
4073
4074    proptest! {
4075        #[test]
4076        fn append_record_batch_metered_bytes_is_sum_of_records(
4077            records in prop::collection::vec(append_record_parts_strategy(), 1..=32),
4078        ) {
4079            let expected = records
4080                .iter()
4081                .map(|(body, headers)| expected_metered_bytes(body, headers))
4082                .sum::<usize>();
4083            let records = records
4084                .into_iter()
4085                .map(|(body, headers)| {
4086                    AppendRecord::new(body)
4087                        .unwrap()
4088                        .with_headers(headers_from_parts(&headers))
4089                        .unwrap()
4090                })
4091                .collect::<Vec<_>>();
4092
4093            let batch = AppendRecordBatch::try_from_iter(records).unwrap();
4094            prop_assert_eq!(batch.metered_bytes(), expected);
4095            prop_assert_eq!(batch.iter().map(MeteredBytes::metered_bytes).sum::<usize>(), expected);
4096        }
4097    }
4098
4099    // -- CommandRecord --
4100
4101    #[test]
4102    fn command_record_fence() {
4103        let token: FencingToken = "tok".parse().unwrap();
4104        let cmd = CommandRecord::fence(token);
4105        let record: AppendRecord = cmd.into();
4106        assert_eq!(record.headers().len(), 1);
4107        assert_eq!(record.headers()[0].name.as_ref(), b"");
4108        assert_eq!(record.headers()[0].value.as_ref(), b"fence");
4109        assert_eq!(record.body(), b"tok");
4110    }
4111
4112    #[test]
4113    fn command_record_trim() {
4114        let cmd = CommandRecord::trim(42);
4115        let record: AppendRecord = cmd.into();
4116        assert_eq!(record.headers().len(), 1);
4117        assert_eq!(record.headers()[0].value.as_ref(), b"trim");
4118        assert_eq!(record.body(), &42u64.to_be_bytes());
4119    }
4120
4121    // -- SequencedRecord --
4122
4123    #[rstest]
4124    #[case::command(vec![Header::new("", "fence")], true)]
4125    #[case::regular(vec![Header::new("key", "value")], false)]
4126    #[case::no_headers(vec![], false)]
4127    fn sequenced_record_command_detection(#[case] headers: Vec<Header>, #[case] expected: bool) {
4128        let record = SequencedRecord {
4129            seq_num: 0,
4130            body: Bytes::from("data"),
4131            headers,
4132            timestamp: 0,
4133        };
4134        assert_eq!(record.is_command_record(), expected);
4135    }
4136
4137    // -- ReadStart --
4138
4139    proptest! {
4140        #[test]
4141        fn read_start_to_api_sets_only_selected_position_field(
4142            from in read_from_strategy(),
4143            clamp_to_tail in any::<bool>(),
4144        ) {
4145            let (seq_num, timestamp, tail_offset) = match from {
4146                ReadFrom::SeqNum(value) => (Some(value), None, None),
4147                ReadFrom::Timestamp(value) => (None, Some(value), None),
4148                ReadFrom::TailOffset(value) => (None, None, Some(value)),
4149            };
4150            let api: api::stream::ReadStart = ReadStart::new()
4151                .with_from(from)
4152                .with_clamp_to_tail(clamp_to_tail)
4153                .into();
4154
4155            prop_assert_eq!(api.seq_num, seq_num);
4156            prop_assert_eq!(api.timestamp, timestamp);
4157            prop_assert_eq!(api.tail_offset, tail_offset);
4158            prop_assert_eq!(api.clamp, clamp_to_tail.then_some(true));
4159        }
4160    }
4161
4162    // -- ReadStop --
4163
4164    #[test]
4165    fn read_stop_to_api() {
4166        let stop = ReadStop::new()
4167            .with_limits(ReadLimits::new().with_count(50))
4168            .with_until(..1000)
4169            .with_wait(30);
4170        let api: api::stream::ReadEnd = stop.into();
4171        assert_eq!(api.count, Some(50));
4172        assert_eq!(api.until, Some(1000));
4173        assert_eq!(api.wait, Some(30));
4174    }
4175
4176    // -- Operation roundtrip --
4177
4178    #[test]
4179    fn operation_roundtrip_all_variants() {
4180        let variants = [
4181            Operation::ListBasins,
4182            Operation::CreateBasin,
4183            Operation::GetBasinConfig,
4184            Operation::DeleteBasin,
4185            Operation::ReconfigureBasin,
4186            Operation::ListAccessTokens,
4187            Operation::IssueAccessToken,
4188            Operation::RevokeAccessToken,
4189            Operation::GetAccountMetrics,
4190            Operation::GetBasinMetrics,
4191            Operation::GetStreamMetrics,
4192            Operation::ListStreams,
4193            Operation::CreateStream,
4194            Operation::GetStreamConfig,
4195            Operation::DeleteStream,
4196            Operation::ReconfigureStream,
4197            Operation::CheckTail,
4198            Operation::Append,
4199            Operation::Read,
4200            Operation::Trim,
4201            Operation::Fence,
4202            Operation::ListLocations,
4203            Operation::GetDefaultLocation,
4204            Operation::SetDefaultLocation,
4205        ];
4206        for op in variants {
4207            let api_op: api::access::Operation = op.into();
4208            let back: Operation = api_op.into();
4209            assert_eq!(back, op);
4210        }
4211    }
4212
4213    // -- MetricUnit --
4214
4215    #[test]
4216    fn metric_unit_conversion() {
4217        assert_eq!(
4218            MetricUnit::from(api::metrics::MetricUnit::Bytes),
4219            MetricUnit::Bytes
4220        );
4221        assert_eq!(
4222            MetricUnit::from(api::metrics::MetricUnit::Operations),
4223            MetricUnit::Operations
4224        );
4225    }
4226
4227    // -- AppendAck --
4228
4229    proptest! {
4230        #[test]
4231        fn append_ack_from_proto_preserves_present_positions_and_defaults_missing(
4232            start in proptest::option::of(proto_stream_position_strategy()),
4233            end in proptest::option::of(proto_stream_position_strategy()),
4234            tail in proptest::option::of(proto_stream_position_strategy()),
4235        ) {
4236            let expected_start = start.unwrap_or_default();
4237            let expected_end = end.unwrap_or_default();
4238            let expected_tail = tail.unwrap_or_default();
4239            let ack: AppendAck = api::stream::proto::AppendAck { start, end, tail }.into();
4240
4241            prop_assert_eq!(ack.start.seq_num, expected_start.seq_num);
4242            prop_assert_eq!(ack.start.timestamp, expected_start.timestamp);
4243            prop_assert_eq!(ack.end.seq_num, expected_end.seq_num);
4244            prop_assert_eq!(ack.end.timestamp, expected_end.timestamp);
4245            prop_assert_eq!(ack.tail.seq_num, expected_tail.seq_num);
4246            prop_assert_eq!(ack.tail.timestamp, expected_tail.timestamp);
4247        }
4248    }
4249
4250    // -- ReadBatch --
4251
4252    #[test]
4253    fn read_batch_from_api() {
4254        let proto_batch = api::stream::proto::ReadBatch {
4255            records: vec![api::stream::proto::SequencedRecord {
4256                seq_num: 0,
4257                body: Bytes::from("hi"),
4258                headers: vec![api::stream::proto::Header {
4259                    name: Bytes::from("k"),
4260                    value: Bytes::from("v"),
4261                }],
4262                timestamp: 42,
4263            }],
4264            tail: Some(api::stream::proto::StreamPosition {
4265                seq_num: 1,
4266                timestamp: 42,
4267            }),
4268        };
4269        let batch = ReadBatch::from_api(proto_batch);
4270        assert_eq!(batch.records.len(), 1);
4271        assert_eq!(batch.records[0].seq_num, 0);
4272        assert_eq!(batch.records[0].timestamp, 42);
4273        assert_eq!(batch.records[0].body.as_ref(), b"hi");
4274        assert_eq!(batch.records[0].headers.len(), 1);
4275        assert_eq!(batch.records[0].headers[0].name.as_ref(), b"k");
4276        assert_eq!(batch.records[0].headers[0].value.as_ref(), b"v");
4277        assert_eq!(
4278            batch.tail,
4279            Some(StreamPosition {
4280                seq_num: 1,
4281                timestamp: 42,
4282            })
4283        );
4284    }
4285
4286    // -- CreateBasinInput --
4287
4288    #[test]
4289    fn create_basin_input_to_api() {
4290        let name: BasinName = "test-basin-name".parse().unwrap();
4291        let input = CreateBasinInput::new(name.clone()).with_config(BasinConfig::new());
4292        let (req, token): (api::basin::CreateBasinRequest, String) = input.into();
4293        assert_eq!(req.basin, name);
4294        assert!(req.config.is_some());
4295        assert!(!token.is_empty());
4296    }
4297
4298    // -- CreateStreamInput --
4299
4300    #[test]
4301    fn create_stream_input_to_api() {
4302        let name: StreamName = "my-stream".parse().unwrap();
4303        let input = CreateStreamInput::new(name.clone()).with_config(StreamConfig::new());
4304        let (req, token): (api::stream::CreateStreamRequest, String) = input.into();
4305        assert_eq!(req.stream, name);
4306        assert!(req.config.is_some());
4307        assert!(!token.is_empty());
4308    }
4309
4310    // -- SequencedRecord from proto --
4311
4312    #[test]
4313    fn sequenced_record_from_proto() {
4314        let proto = api::stream::proto::SequencedRecord {
4315            seq_num: 99,
4316            body: Bytes::from("data"),
4317            headers: vec![api::stream::proto::Header {
4318                name: Bytes::from("k"),
4319                value: Bytes::from("v"),
4320            }],
4321            timestamp: 1234,
4322        };
4323        let record: SequencedRecord = proto.into();
4324        assert_eq!(record.seq_num, 99);
4325        assert_eq!(record.body.as_ref(), b"data");
4326        assert_eq!(record.headers.len(), 1);
4327        assert_eq!(record.headers[0].name.as_ref(), b"k");
4328        assert_eq!(record.headers[0].value.as_ref(), b"v");
4329        assert_eq!(record.timestamp, 1234);
4330    }
4331
4332    // -- S2Error from ApiError --
4333
4334    #[test]
4335    fn s2_error_from_api_error_client() {
4336        let err = ApiError::Client(ClientError::Others("client error".to_owned()));
4337        let s2_err: S2Error = err.into();
4338        assert!(matches!(s2_err, S2Error::Client(_)));
4339    }
4340
4341    // -- ErrorResponse --
4342
4343    #[test]
4344    fn error_response_from_api() {
4345        let api_resp = ApiErrorResponse {
4346            code: "not_found".to_string(),
4347            message: "basin not found".to_string(),
4348        };
4349        let resp: ErrorResponse = api_resp.into();
4350        assert_eq!(resp.code, "not_found");
4351        assert_eq!(resp.message, "basin not found");
4352        assert!(resp.to_string().contains("not_found"));
4353    }
4354}