1use std::{
4 collections::HashSet,
5 env::VarError,
6 fmt,
7 num::NonZeroU32,
8 ops::{Deref, RangeTo},
9 pin::Pin,
10 str::FromStr,
11 sync::Arc,
12 time::Duration,
13};
14
15#[cfg(feature = "_hidden")]
16use async_trait::async_trait;
17use bytes::Bytes;
18use http::{
19 HeaderMap,
20 header::HeaderValue,
21 uri::{Authority, Scheme},
22};
23use rand::RngExt;
24use s2_api::{v1 as api, v1::stream::s2s::CompressionAlgorithm};
25pub use s2_common::ValidationError;
27pub use s2_common::access::AccessTokenId;
32pub use s2_common::access::AccessTokenIdPrefix;
34pub use s2_common::access::AccessTokenIdStartAfter;
36pub use s2_common::basin::BasinName;
41pub use s2_common::basin::BasinNamePrefix;
43pub use s2_common::basin::BasinNameStartAfter;
45pub use s2_common::location::LocationName;
50pub use s2_common::stream::StreamName;
55pub use s2_common::stream::StreamNamePrefix;
57pub use s2_common::stream::StreamNameStartAfter;
59pub use s2_common::{
60 caps::RECORD_BATCH_MAX,
61 encryption::{EncryptionAlgorithm, EncryptionKey},
62};
63
64pub(crate) const ONE_MIB: u32 = 1024 * 1024;
65
66use s2_common::{
67 maybe::Maybe,
68 record::{MAX_FENCING_TOKEN_LENGTH, Metered, MeteredSize},
69 resources::ProvisionResult,
70};
71use secrecy::SecretString;
72
73use crate::error::RequestError;
74
75#[cfg(feature = "_hidden")]
76#[derive(Debug, Clone, thiserror::Error)]
77#[error("{message}")]
78#[doc(hidden)]
79pub struct AccessTokenProviderError {
80 message: String,
81 retryable: bool,
82}
83
84#[cfg(feature = "_hidden")]
85impl AccessTokenProviderError {
86 pub fn transient(message: impl Into<String>) -> Self {
88 Self {
89 message: message.into(),
90 retryable: true,
91 }
92 }
93
94 pub fn permanent(message: impl Into<String>) -> Self {
96 Self {
97 message: message.into(),
98 retryable: false,
99 }
100 }
101
102 pub(crate) fn is_retryable(&self) -> bool {
103 self.retryable
104 }
105}
106
107#[cfg(feature = "_hidden")]
108#[async_trait]
109#[doc(hidden)]
110pub trait AccessTokenProvider: fmt::Debug + Send + Sync {
111 async fn access_token(&self) -> Result<String, AccessTokenProviderError>;
113
114 fn invalidate_access_token(&self, _rejected_access_token: &str) {}
116}
117
118#[derive(Clone)]
119pub(crate) enum AccessToken {
120 Static(SecretString),
121 #[cfg(feature = "_hidden")]
122 Provider(Arc<dyn AccessTokenProvider>),
123}
124
125#[derive(Debug, Clone, Copy, PartialEq, Eq)]
126pub(crate) enum AccessTokenMode {
127 Static,
128 #[cfg(feature = "_hidden")]
129 Refreshable,
130}
131
132impl AccessTokenMode {
133 pub(crate) fn is_refreshable(self) -> bool {
134 match self {
135 Self::Static => false,
136 #[cfg(feature = "_hidden")]
137 Self::Refreshable => true,
138 }
139 }
140}
141
142impl AccessToken {
143 pub(crate) fn mode(&self) -> AccessTokenMode {
144 match self {
145 Self::Static(_) => AccessTokenMode::Static,
146 #[cfg(feature = "_hidden")]
147 Self::Provider(_) => AccessTokenMode::Refreshable,
148 }
149 }
150}
151
152impl fmt::Debug for AccessToken {
153 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
154 match self {
155 Self::Static(_) => formatter.write_str("Static(<redacted>)"),
156 #[cfg(feature = "_hidden")]
157 Self::Provider(_) => formatter.write_str("Provider(<redacted>)"),
158 }
159 }
160}
161
162#[derive(Debug, Clone, Copy, PartialEq, Eq)]
168pub struct S2DateTime(time::OffsetDateTime);
169
170impl TryFrom<time::OffsetDateTime> for S2DateTime {
171 type Error = ValidationError;
172
173 fn try_from(dt: time::OffsetDateTime) -> Result<Self, Self::Error> {
174 dt.format(&time::format_description::well_known::Rfc3339)
175 .map_err(|e| ValidationError(format!("not a valid RFC 3339 datetime: {e}")))?;
176 Ok(Self(dt))
177 }
178}
179
180impl From<S2DateTime> for time::OffsetDateTime {
181 fn from(dt: S2DateTime) -> Self {
182 dt.0
183 }
184}
185
186impl FromStr for S2DateTime {
187 type Err = ValidationError;
188
189 fn from_str(s: &str) -> Result<Self, Self::Err> {
190 time::OffsetDateTime::parse(s, &time::format_description::well_known::Rfc3339)
191 .map(Self)
192 .map_err(|e| ValidationError(format!("not a valid RFC 3339 datetime: {e}")))
193 }
194}
195
196impl fmt::Display for S2DateTime {
197 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
198 write!(
199 f,
200 "{}",
201 self.0
202 .format(&time::format_description::well_known::Rfc3339)
203 .expect("RFC3339 formatting should not fail for S2DateTime")
204 )
205 }
206}
207
208#[derive(Debug, Clone, PartialEq)]
210pub(crate) enum BasinAuthority {
211 ParentZone(Authority),
213 Direct(Authority),
215}
216
217#[derive(Debug, Clone)]
219pub struct AccountEndpoint {
220 scheme: Scheme,
221 authority: Authority,
222}
223
224impl AccountEndpoint {
225 pub fn new(endpoint: &str) -> Result<Self, ValidationError> {
227 endpoint.parse()
228 }
229}
230
231impl FromStr for AccountEndpoint {
232 type Err = ValidationError;
233
234 fn from_str(s: &str) -> Result<Self, Self::Err> {
235 let (scheme, authority) = match s.find("://") {
236 Some(idx) => {
237 let scheme: Scheme = s[..idx]
238 .parse()
239 .map_err(|_| "invalid account endpoint scheme".to_string())?;
240 (scheme, &s[idx + 3..])
241 }
242 None => (Scheme::HTTPS, s),
243 };
244 Ok(Self {
245 scheme,
246 authority: authority
247 .parse()
248 .map_err(|e| format!("invalid account endpoint authority: {e}"))?,
249 })
250 }
251}
252
253#[derive(Debug, Clone)]
255pub struct BasinEndpoint {
256 scheme: Scheme,
257 authority: BasinAuthority,
258}
259
260impl BasinEndpoint {
261 pub fn new(endpoint: &str) -> Result<Self, ValidationError> {
263 endpoint.parse()
264 }
265}
266
267impl FromStr for BasinEndpoint {
268 type Err = ValidationError;
269
270 fn from_str(s: &str) -> Result<Self, Self::Err> {
271 let (scheme, authority) = match s.find("://") {
272 Some(idx) => {
273 let scheme: Scheme = s[..idx]
274 .parse()
275 .map_err(|_| "invalid basin endpoint scheme".to_string())?;
276 (scheme, &s[idx + 3..])
277 }
278 None => (Scheme::HTTPS, s),
279 };
280 let authority = if let Some(authority) = authority.strip_prefix("{basin}.") {
281 BasinAuthority::ParentZone(
282 authority
283 .parse()
284 .map_err(|e| format!("invalid basin endpoint authority: {e}"))?,
285 )
286 } else {
287 BasinAuthority::Direct(
288 authority
289 .parse()
290 .map_err(|e| format!("invalid basin endpoint authority: {e}"))?,
291 )
292 };
293 Ok(Self { scheme, authority })
294 }
295}
296
297#[derive(Debug, Clone)]
298#[non_exhaustive]
299pub struct S2Endpoints {
301 pub(crate) scheme: Scheme,
302 pub(crate) account_authority: Authority,
303 pub(crate) basin_authority: BasinAuthority,
304}
305
306impl S2Endpoints {
307 pub fn new(
309 account_endpoint: AccountEndpoint,
310 basin_endpoint: BasinEndpoint,
311 ) -> Result<Self, ValidationError> {
312 if account_endpoint.scheme != basin_endpoint.scheme {
313 return Err("account and basin endpoints must have the same scheme".into());
314 }
315 Ok(Self {
316 scheme: account_endpoint.scheme,
317 account_authority: account_endpoint.authority,
318 basin_authority: basin_endpoint.authority,
319 })
320 }
321
322 pub fn for_endpoint(endpoint: &str) -> Result<Self, ValidationError> {
326 Self::new(
327 AccountEndpoint::new(endpoint)?,
328 BasinEndpoint::new(endpoint)?,
329 )
330 }
331
332 pub fn from_env() -> Result<Self, ValidationError> {
338 let account_endpoint: AccountEndpoint = match std::env::var("S2_ACCOUNT_ENDPOINT") {
339 Ok(endpoint) => endpoint.parse()?,
340 Err(VarError::NotPresent) => return Err("S2_ACCOUNT_ENDPOINT env var not set".into()),
341 Err(VarError::NotUnicode(_)) => {
342 return Err("S2_ACCOUNT_ENDPOINT is not valid unicode".into());
343 }
344 };
345
346 let basin_endpoint: BasinEndpoint = match std::env::var("S2_BASIN_ENDPOINT") {
347 Ok(endpoint) => endpoint.parse()?,
348 Err(VarError::NotPresent) => return Err("S2_BASIN_ENDPOINT env var not set".into()),
349 Err(VarError::NotUnicode(_)) => {
350 return Err("S2_BASIN_ENDPOINT is not valid unicode".into());
351 }
352 };
353
354 if account_endpoint.scheme != basin_endpoint.scheme {
355 return Err(
356 "S2_ACCOUNT_ENDPOINT and S2_BASIN_ENDPOINT must have the same scheme".into(),
357 );
358 }
359
360 Ok(Self {
361 scheme: account_endpoint.scheme,
362 account_authority: account_endpoint.authority,
363 basin_authority: basin_endpoint.authority,
364 })
365 }
366
367 pub fn for_cloud() -> Self {
369 Self {
370 scheme: Scheme::HTTPS,
371 account_authority: "a.s2.dev".try_into().expect("valid authority"),
372 basin_authority: BasinAuthority::ParentZone(
373 "b.s2.dev".try_into().expect("valid authority"),
374 ),
375 }
376 }
377}
378
379#[derive(Debug, Clone, Copy)]
380pub enum Compression {
382 None,
384 Gzip,
386 Zstd,
388}
389
390impl From<Compression> for CompressionAlgorithm {
391 fn from(value: Compression) -> Self {
392 match value {
393 Compression::None => CompressionAlgorithm::None,
394 Compression::Gzip => CompressionAlgorithm::Gzip,
395 Compression::Zstd => CompressionAlgorithm::Zstd,
396 }
397 }
398}
399
400#[derive(Debug, Clone, Copy, PartialEq)]
401#[non_exhaustive]
402pub enum AppendRetryPolicy {
405 All,
407 NoSideEffects,
417}
418
419#[derive(Debug, Clone)]
420#[non_exhaustive]
421pub struct RetryConfig {
430 pub max_attempts: NonZeroU32,
434 pub min_base_delay: Duration,
438 pub max_base_delay: Duration,
442 pub append_retry_policy: AppendRetryPolicy,
447}
448
449impl Default for RetryConfig {
450 fn default() -> Self {
451 Self {
452 max_attempts: NonZeroU32::new(3).expect("valid non-zero u32"),
453 min_base_delay: Duration::from_millis(100),
454 max_base_delay: Duration::from_secs(1),
455 append_retry_policy: AppendRetryPolicy::All,
456 }
457 }
458}
459
460impl RetryConfig {
461 pub fn new() -> Self {
463 Self::default()
464 }
465
466 pub(crate) fn max_retries(&self) -> u32 {
467 self.max_attempts.get() - 1
468 }
469
470 pub fn with_max_attempts(self, max_attempts: NonZeroU32) -> Self {
472 Self {
473 max_attempts,
474 ..self
475 }
476 }
477
478 pub fn with_min_base_delay(self, min_base_delay: Duration) -> Self {
480 Self {
481 min_base_delay,
482 ..self
483 }
484 }
485
486 pub fn with_max_base_delay(self, max_base_delay: Duration) -> Self {
488 Self {
489 max_base_delay,
490 ..self
491 }
492 }
493
494 pub fn with_append_retry_policy(self, append_retry_policy: AppendRetryPolicy) -> Self {
497 Self {
498 append_retry_policy,
499 ..self
500 }
501 }
502}
503
504#[derive(Debug, Clone)]
505#[non_exhaustive]
506pub struct S2Config {
508 pub(crate) access_token: AccessToken,
509 pub(crate) endpoints: S2Endpoints,
510 pub(crate) connection_timeout: Duration,
511 pub(crate) request_timeout: Duration,
512 pub(crate) retry: RetryConfig,
513 pub(crate) compression: Compression,
514 pub(crate) user_agent: HeaderValue,
515 pub(crate) default_headers: HeaderMap,
516 pub(crate) insecure_skip_cert_verification: bool,
517 pub(crate) rustls_crypto_provider: Option<Arc<rustls::crypto::CryptoProvider>>,
518}
519
520impl S2Config {
521 pub fn new(access_token: impl Into<String>) -> Self {
523 Self {
524 access_token: AccessToken::Static(access_token.into().into()),
525 endpoints: S2Endpoints::for_cloud(),
526 connection_timeout: Duration::from_secs(3),
527 request_timeout: Duration::from_secs(5),
528 retry: RetryConfig::new(),
529 compression: Compression::None,
530 user_agent: concat!("s2-sdk-rust/", env!("CARGO_PKG_VERSION"))
531 .parse()
532 .expect("valid user agent"),
533 default_headers: HeaderMap::new(),
534 insecure_skip_cert_verification: false,
535 rustls_crypto_provider: default_rustls_crypto_provider(),
536 }
537 }
538
539 #[cfg(feature = "_hidden")]
540 #[doc(hidden)]
541 pub fn with_access_token_provider(self, provider: impl AccessTokenProvider + 'static) -> Self {
542 Self {
543 access_token: AccessToken::Provider(Arc::new(provider)),
544 ..self
545 }
546 }
547
548 pub fn with_endpoints(self, endpoints: S2Endpoints) -> Self {
550 Self { endpoints, ..self }
551 }
552
553 #[cfg(feature = "_hidden")]
575 #[doc(hidden)]
576 pub fn with_default_headers(self, default_headers: HeaderMap) -> Result<Self, ValidationError> {
577 if default_headers.contains_key(http::header::CONTENT_ENCODING) {
578 return Err(ValidationError(
579 "Content-Encoding cannot be set in default headers; use S2Config::with_compression instead"
580 .into(),
581 ));
582 }
583 for name in [
584 http::header::CONTENT_LENGTH,
585 http::header::TRANSFER_ENCODING,
586 ] {
587 if default_headers.contains_key(&name) {
588 return Err(ValidationError(format!(
589 "{name} cannot be set in default headers; the SDK controls request body framing"
590 )));
591 }
592 }
593 Ok(Self {
594 default_headers,
595 ..self
596 })
597 }
598
599 pub fn with_connection_timeout(self, connection_timeout: Duration) -> Self {
603 Self {
604 connection_timeout,
605 ..self
606 }
607 }
608
609 pub fn with_request_timeout(self, request_timeout: Duration) -> Self {
613 Self {
614 request_timeout,
615 ..self
616 }
617 }
618
619 pub fn with_retry(self, retry: RetryConfig) -> Self {
623 Self { retry, ..self }
624 }
625
626 pub fn with_compression(self, compression: Compression) -> Self {
630 Self {
631 compression,
632 ..self
633 }
634 }
635
636 pub fn with_insecure_skip_cert_verification(self, skip: bool) -> Self {
648 Self {
649 insecure_skip_cert_verification: skip,
650 ..self
651 }
652 }
653
654 pub fn with_rustls_crypto_provider(
664 self,
665 provider: impl Into<Arc<rustls::crypto::CryptoProvider>>,
666 ) -> Self {
667 Self {
668 rustls_crypto_provider: Some(provider.into()),
669 ..self
670 }
671 }
672
673 #[cfg(feature = "rustls-aws-lc-rs")]
677 pub fn with_rustls_aws_lc_rs_crypto_provider(self) -> Self {
678 self.with_rustls_crypto_provider(rustls::crypto::aws_lc_rs::default_provider())
679 }
680
681 #[cfg(feature = "rustls-ring")]
685 pub fn with_rustls_ring_crypto_provider(self) -> Self {
686 self.with_rustls_crypto_provider(rustls::crypto::ring::default_provider())
687 }
688
689 #[doc(hidden)]
690 #[cfg(feature = "_hidden")]
691 pub fn with_user_agent(self, user_agent: impl Into<String>) -> Result<Self, ValidationError> {
692 let user_agent = user_agent
693 .into()
694 .parse()
695 .map_err(|e| ValidationError(format!("invalid user agent: {e}")))?;
696 Ok(Self { user_agent, ..self })
697 }
698}
699
700#[cfg(feature = "rustls-aws-lc-rs")]
701fn default_rustls_crypto_provider() -> Option<Arc<rustls::crypto::CryptoProvider>> {
702 Some(Arc::new(rustls::crypto::aws_lc_rs::default_provider()))
703}
704
705#[cfg(all(not(feature = "rustls-aws-lc-rs"), feature = "rustls-ring"))]
706fn default_rustls_crypto_provider() -> Option<Arc<rustls::crypto::CryptoProvider>> {
707 Some(Arc::new(rustls::crypto::ring::default_provider()))
708}
709
710#[cfg(all(not(feature = "rustls-aws-lc-rs"), not(feature = "rustls-ring")))]
711fn default_rustls_crypto_provider() -> Option<Arc<rustls::crypto::CryptoProvider>> {
712 None
713}
714
715#[derive(Debug, Default, Clone, PartialEq, Eq)]
716#[non_exhaustive]
717pub struct Page<T> {
719 pub values: Vec<T>,
721 pub has_more: bool,
723}
724
725impl<T> Page<T> {
726 pub(crate) fn new(values: impl Into<Vec<T>>, has_more: bool) -> Self {
727 Self {
728 values: values.into(),
729 has_more,
730 }
731 }
732}
733
734#[derive(Debug, Clone, Copy, PartialEq, Eq)]
735pub enum StorageClass {
737 Standard,
739 Express,
741}
742
743impl From<api::config::StorageClass> for StorageClass {
744 fn from(value: api::config::StorageClass) -> Self {
745 match value {
746 api::config::StorageClass::Standard => StorageClass::Standard,
747 api::config::StorageClass::Express => StorageClass::Express,
748 }
749 }
750}
751
752impl From<StorageClass> for api::config::StorageClass {
753 fn from(value: StorageClass) -> Self {
754 match value {
755 StorageClass::Standard => api::config::StorageClass::Standard,
756 StorageClass::Express => api::config::StorageClass::Express,
757 }
758 }
759}
760
761#[derive(Debug, Clone, Copy, PartialEq, Eq)]
762pub enum RetentionPolicy {
764 Age(u64),
766 Infinite,
768}
769
770impl From<api::config::RetentionPolicy> for RetentionPolicy {
771 fn from(value: api::config::RetentionPolicy) -> Self {
772 match value {
773 api::config::RetentionPolicy::Age(secs) => RetentionPolicy::Age(secs),
774 api::config::RetentionPolicy::Infinite(_) => RetentionPolicy::Infinite,
775 }
776 }
777}
778
779impl From<RetentionPolicy> for api::config::RetentionPolicy {
780 fn from(value: RetentionPolicy) -> Self {
781 match value {
782 RetentionPolicy::Age(secs) => api::config::RetentionPolicy::Age(secs),
783 RetentionPolicy::Infinite => {
784 api::config::RetentionPolicy::Infinite(api::config::InfiniteRetention {})
785 }
786 }
787 }
788}
789
790#[derive(Debug, Clone, Copy, PartialEq, Eq)]
791pub enum TimestampingMode {
793 ClientPrefer,
795 ClientRequire,
797 Arrival,
799}
800
801impl From<api::config::TimestampingMode> for TimestampingMode {
802 fn from(value: api::config::TimestampingMode) -> Self {
803 match value {
804 api::config::TimestampingMode::ClientPrefer => TimestampingMode::ClientPrefer,
805 api::config::TimestampingMode::ClientRequire => TimestampingMode::ClientRequire,
806 api::config::TimestampingMode::Arrival => TimestampingMode::Arrival,
807 }
808 }
809}
810
811impl From<TimestampingMode> for api::config::TimestampingMode {
812 fn from(value: TimestampingMode) -> Self {
813 match value {
814 TimestampingMode::ClientPrefer => api::config::TimestampingMode::ClientPrefer,
815 TimestampingMode::ClientRequire => api::config::TimestampingMode::ClientRequire,
816 TimestampingMode::Arrival => api::config::TimestampingMode::Arrival,
817 }
818 }
819}
820
821#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
822#[non_exhaustive]
823pub struct TimestampingConfig {
825 pub mode: Option<TimestampingMode>,
829 pub uncapped: Option<bool>,
833}
834
835impl TimestampingConfig {
836 pub fn new() -> Self {
838 Self::default()
839 }
840
841 pub fn with_mode(self, mode: TimestampingMode) -> Self {
843 Self {
844 mode: Some(mode),
845 ..self
846 }
847 }
848
849 pub fn with_uncapped(self, uncapped: bool) -> Self {
851 Self {
852 uncapped: Some(uncapped),
853 ..self
854 }
855 }
856}
857
858impl From<api::config::TimestampingConfig> for TimestampingConfig {
859 fn from(value: api::config::TimestampingConfig) -> Self {
860 Self {
861 mode: value.mode.map(Into::into),
862 uncapped: value.uncapped,
863 }
864 }
865}
866
867impl From<TimestampingConfig> for api::config::TimestampingConfig {
868 fn from(value: TimestampingConfig) -> Self {
869 Self {
870 mode: value.mode.map(Into::into),
871 uncapped: value.uncapped,
872 }
873 }
874}
875
876#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
877#[non_exhaustive]
878pub struct DeleteOnEmptyConfig {
880 pub min_age_secs: u64,
884}
885
886impl DeleteOnEmptyConfig {
887 pub fn new() -> Self {
889 Self::default()
890 }
891
892 pub fn with_min_age(self, min_age: Duration) -> Self {
894 Self {
895 min_age_secs: min_age.as_secs(),
896 }
897 }
898}
899
900impl From<api::config::DeleteOnEmptyConfig> for DeleteOnEmptyConfig {
901 fn from(value: api::config::DeleteOnEmptyConfig) -> Self {
902 Self {
903 min_age_secs: value.min_age_secs,
904 }
905 }
906}
907
908impl From<DeleteOnEmptyConfig> for api::config::DeleteOnEmptyConfig {
909 fn from(value: DeleteOnEmptyConfig) -> Self {
910 Self {
911 min_age_secs: value.min_age_secs,
912 }
913 }
914}
915
916#[derive(Debug, Clone, Default, PartialEq, Eq)]
917#[non_exhaustive]
918pub struct StreamConfig {
920 pub storage_class: Option<StorageClass>,
924 pub retention_policy: Option<RetentionPolicy>,
928 pub timestamping: Option<TimestampingConfig>,
932 pub delete_on_empty: Option<DeleteOnEmptyConfig>,
936}
937
938impl StreamConfig {
939 pub fn new() -> Self {
941 Self::default()
942 }
943
944 pub fn with_storage_class(self, storage_class: StorageClass) -> Self {
946 Self {
947 storage_class: Some(storage_class),
948 ..self
949 }
950 }
951
952 pub fn with_retention_policy(self, retention_policy: RetentionPolicy) -> Self {
954 Self {
955 retention_policy: Some(retention_policy),
956 ..self
957 }
958 }
959
960 pub fn with_timestamping(self, timestamping: TimestampingConfig) -> Self {
962 Self {
963 timestamping: Some(timestamping),
964 ..self
965 }
966 }
967
968 pub fn with_delete_on_empty(self, delete_on_empty: DeleteOnEmptyConfig) -> Self {
970 Self {
971 delete_on_empty: Some(delete_on_empty),
972 ..self
973 }
974 }
975}
976
977impl From<api::config::StreamConfig> for StreamConfig {
978 fn from(value: api::config::StreamConfig) -> Self {
979 Self {
980 storage_class: value.storage_class.map(Into::into),
981 retention_policy: value.retention_policy.map(Into::into),
982 timestamping: value.timestamping.map(Into::into),
983 delete_on_empty: value.delete_on_empty.map(Into::into),
984 }
985 }
986}
987
988impl From<StreamConfig> for api::config::StreamConfig {
989 fn from(value: StreamConfig) -> Self {
990 Self {
991 storage_class: value.storage_class.map(Into::into),
992 retention_policy: value.retention_policy.map(Into::into),
993 timestamping: value.timestamping.map(Into::into),
994 delete_on_empty: value.delete_on_empty.map(Into::into),
995 }
996 }
997}
998
999#[derive(Debug, Clone, Default, PartialEq, Eq)]
1000#[non_exhaustive]
1001pub struct BasinConfig {
1003 pub default_stream_config: Option<StreamConfig>,
1007 pub stream_cipher: Option<EncryptionAlgorithm>,
1009 pub create_stream_on_append: bool,
1013 pub create_stream_on_read: bool,
1017}
1018
1019impl BasinConfig {
1020 pub fn new() -> Self {
1022 Self::default()
1023 }
1024
1025 pub fn with_default_stream_config(self, config: StreamConfig) -> Self {
1027 Self {
1028 default_stream_config: Some(config),
1029 ..self
1030 }
1031 }
1032
1033 pub fn with_stream_cipher(self, stream_cipher: EncryptionAlgorithm) -> Self {
1035 Self {
1036 stream_cipher: Some(stream_cipher),
1037 ..self
1038 }
1039 }
1040
1041 pub fn with_create_stream_on_append(self, create_stream_on_append: bool) -> Self {
1044 Self {
1045 create_stream_on_append,
1046 ..self
1047 }
1048 }
1049
1050 pub fn with_create_stream_on_read(self, create_stream_on_read: bool) -> Self {
1052 Self {
1053 create_stream_on_read,
1054 ..self
1055 }
1056 }
1057}
1058
1059impl From<api::config::BasinConfig> for BasinConfig {
1060 fn from(value: api::config::BasinConfig) -> Self {
1061 Self {
1062 default_stream_config: value.default_stream_config.map(Into::into),
1063 stream_cipher: value.stream_cipher.map(Into::into),
1064 create_stream_on_append: value.create_stream_on_append,
1065 create_stream_on_read: value.create_stream_on_read,
1066 }
1067 }
1068}
1069
1070impl From<BasinConfig> for api::config::BasinConfig {
1071 fn from(value: BasinConfig) -> Self {
1072 Self {
1073 default_stream_config: value.default_stream_config.map(Into::into),
1074 stream_cipher: value.stream_cipher.map(Into::into),
1075 create_stream_on_append: value.create_stream_on_append,
1076 create_stream_on_read: value.create_stream_on_read,
1077 }
1078 }
1079}
1080
1081#[derive(Debug, Clone)]
1082#[non_exhaustive]
1083pub struct CreateBasinInput {
1085 pub name: BasinName,
1087 pub config: Option<BasinConfig>,
1091 pub location: Option<LocationName>,
1095 idempotency_token: String,
1096}
1097
1098impl CreateBasinInput {
1099 pub fn new(name: BasinName) -> Self {
1101 Self {
1102 name,
1103 config: None,
1104 location: None,
1105 idempotency_token: idempotency_token(),
1106 }
1107 }
1108
1109 pub fn with_config(self, config: BasinConfig) -> Self {
1111 Self {
1112 config: Some(config),
1113 ..self
1114 }
1115 }
1116
1117 pub fn with_location<S>(self, location: S) -> Result<Self, ValidationError>
1119 where
1120 S: TryInto<LocationName>,
1121 S::Error: fmt::Display,
1122 {
1123 let location = location
1124 .try_into()
1125 .map_err(|e| ValidationError(e.to_string()))?;
1126 Ok(Self {
1127 location: Some(location),
1128 ..self
1129 })
1130 }
1131}
1132
1133impl From<CreateBasinInput> for (api::basin::CreateBasinRequest, String) {
1134 fn from(value: CreateBasinInput) -> Self {
1135 (
1136 api::basin::CreateBasinRequest {
1137 basin: value.name,
1138 config: value.config.map(Into::into),
1139 location: value.location,
1140 },
1141 value.idempotency_token,
1142 )
1143 }
1144}
1145
1146#[derive(Debug, Clone)]
1147#[non_exhaustive]
1148pub struct EnsureBasinInput {
1150 pub name: BasinName,
1152 pub config: Option<BasinConfig>,
1156 pub location: Option<LocationName>,
1161}
1162
1163impl EnsureBasinInput {
1164 pub fn new(name: BasinName) -> Self {
1166 Self {
1167 name,
1168 config: None,
1169 location: None,
1170 }
1171 }
1172
1173 pub fn with_config(self, config: BasinConfig) -> Self {
1175 Self {
1176 config: Some(config),
1177 ..self
1178 }
1179 }
1180
1181 pub fn with_location<S>(self, location: S) -> Result<Self, ValidationError>
1183 where
1184 S: TryInto<LocationName>,
1185 S::Error: fmt::Display,
1186 {
1187 let location = location
1188 .try_into()
1189 .map_err(|e| ValidationError(e.to_string()))?;
1190 Ok(Self {
1191 location: Some(location),
1192 ..self
1193 })
1194 }
1195}
1196
1197impl From<EnsureBasinInput> for (BasinName, Option<api::basin::EnsureBasinRequest>) {
1198 fn from(value: EnsureBasinInput) -> Self {
1199 let config = value.config;
1200 let request = if config.is_some() || value.location.is_some() {
1201 Some(api::basin::EnsureBasinRequest {
1202 config: config.map(Into::into),
1203 location: value.location,
1204 })
1205 } else {
1206 None
1207 };
1208 (value.name, request)
1209 }
1210}
1211
1212#[derive(Debug, Clone)]
1213pub enum EnsureOutput<T> {
1216 Created(T),
1218 ConfigUpdated(T),
1220 ConfigUnchanged(T),
1222}
1223
1224impl<T> From<ProvisionResult<T>> for EnsureOutput<T> {
1225 fn from(result: ProvisionResult<T>) -> Self {
1226 match result {
1227 ProvisionResult::Created(info) => EnsureOutput::Created(info),
1228 ProvisionResult::Updated(info) => EnsureOutput::ConfigUpdated(info),
1229 ProvisionResult::Noop(info) => EnsureOutput::ConfigUnchanged(info),
1230 }
1231 }
1232}
1233
1234#[derive(Debug, Clone, Default)]
1235#[non_exhaustive]
1236pub struct ListBasinsInput {
1238 pub prefix: BasinNamePrefix,
1242 pub start_after: BasinNameStartAfter,
1246 pub limit: Option<usize>,
1250}
1251
1252impl ListBasinsInput {
1253 pub fn new() -> Self {
1255 Self::default()
1256 }
1257
1258 pub fn with_prefix(self, prefix: BasinNamePrefix) -> Self {
1260 Self { prefix, ..self }
1261 }
1262
1263 pub fn with_start_after(self, start_after: BasinNameStartAfter) -> Self {
1266 Self {
1267 start_after,
1268 ..self
1269 }
1270 }
1271
1272 pub fn with_limit(self, limit: usize) -> Self {
1274 Self {
1275 limit: Some(limit),
1276 ..self
1277 }
1278 }
1279}
1280
1281impl From<ListBasinsInput> for api::basin::ListBasinsRequest {
1282 fn from(value: ListBasinsInput) -> Self {
1283 Self {
1284 prefix: Some(value.prefix),
1285 start_after: Some(value.start_after),
1286 limit: value.limit,
1287 }
1288 }
1289}
1290
1291#[derive(Debug, Clone, Default)]
1292pub struct ListAllBasinsInput {
1294 pub prefix: BasinNamePrefix,
1298 pub start_after: BasinNameStartAfter,
1302 pub include_deleted: bool,
1306}
1307
1308impl ListAllBasinsInput {
1309 pub fn new() -> Self {
1311 Self::default()
1312 }
1313
1314 pub fn with_prefix(self, prefix: BasinNamePrefix) -> Self {
1316 Self { prefix, ..self }
1317 }
1318
1319 pub fn with_start_after(self, start_after: BasinNameStartAfter) -> Self {
1322 Self {
1323 start_after,
1324 ..self
1325 }
1326 }
1327
1328 pub fn with_include_deleted(self, include_deleted: bool) -> Self {
1330 Self {
1331 include_deleted,
1332 ..self
1333 }
1334 }
1335}
1336
1337#[derive(Debug, Clone, PartialEq, Eq)]
1338#[non_exhaustive]
1339pub struct BasinInfo {
1341 pub name: BasinName,
1343 pub location: Option<LocationName>,
1345 pub created_at: S2DateTime,
1347 pub deleted_at: Option<S2DateTime>,
1349}
1350
1351impl TryFrom<api::basin::BasinInfo> for BasinInfo {
1352 type Error = ValidationError;
1353
1354 fn try_from(value: api::basin::BasinInfo) -> Result<Self, Self::Error> {
1355 Ok(Self {
1356 name: value.name,
1357 location: value.location,
1358 created_at: value.created_at.try_into()?,
1359 deleted_at: value.deleted_at.map(S2DateTime::try_from).transpose()?,
1360 })
1361 }
1362}
1363
1364#[derive(Debug, Clone)]
1365#[non_exhaustive]
1366pub struct DeleteBasinInput {
1368 pub name: BasinName,
1370 pub ignore_not_found: bool,
1372}
1373
1374impl DeleteBasinInput {
1375 pub fn new(name: BasinName) -> Self {
1377 Self {
1378 name,
1379 ignore_not_found: false,
1380 }
1381 }
1382
1383 pub fn with_ignore_not_found(self, ignore_not_found: bool) -> Self {
1385 Self {
1386 ignore_not_found,
1387 ..self
1388 }
1389 }
1390}
1391
1392#[derive(Debug, Clone, Default)]
1393#[non_exhaustive]
1394pub struct TimestampingReconfiguration {
1396 pub mode: Maybe<Option<TimestampingMode>>,
1398 pub uncapped: Maybe<Option<bool>>,
1400}
1401
1402impl TimestampingReconfiguration {
1403 pub fn new() -> Self {
1405 Self::default()
1406 }
1407
1408 pub fn with_mode(self, mode: TimestampingMode) -> Self {
1410 Self {
1411 mode: Maybe::Specified(Some(mode)),
1412 ..self
1413 }
1414 }
1415
1416 pub fn with_uncapped(self, uncapped: bool) -> Self {
1418 Self {
1419 uncapped: Maybe::Specified(Some(uncapped)),
1420 ..self
1421 }
1422 }
1423}
1424
1425impl From<TimestampingReconfiguration> for api::config::TimestampingReconfiguration {
1426 fn from(value: TimestampingReconfiguration) -> Self {
1427 Self {
1428 mode: value.mode.map(|m| m.map(Into::into)),
1429 uncapped: value.uncapped,
1430 }
1431 }
1432}
1433
1434#[derive(Debug, Clone, Default)]
1435#[non_exhaustive]
1436pub struct DeleteOnEmptyReconfiguration {
1438 pub min_age_secs: Maybe<Option<u64>>,
1440}
1441
1442impl DeleteOnEmptyReconfiguration {
1443 pub fn new() -> Self {
1445 Self::default()
1446 }
1447
1448 pub fn with_min_age(self, min_age: Duration) -> Self {
1450 Self {
1451 min_age_secs: Maybe::Specified(Some(min_age.as_secs())),
1452 }
1453 }
1454}
1455
1456impl From<DeleteOnEmptyReconfiguration> for api::config::DeleteOnEmptyReconfiguration {
1457 fn from(value: DeleteOnEmptyReconfiguration) -> Self {
1458 Self {
1459 min_age_secs: value.min_age_secs,
1460 }
1461 }
1462}
1463
1464#[derive(Debug, Clone, Default)]
1465#[non_exhaustive]
1466pub struct StreamReconfiguration {
1468 pub storage_class: Maybe<Option<StorageClass>>,
1470 pub retention_policy: Maybe<Option<RetentionPolicy>>,
1472 pub timestamping: Maybe<Option<TimestampingReconfiguration>>,
1474 pub delete_on_empty: Maybe<Option<DeleteOnEmptyReconfiguration>>,
1476}
1477
1478impl StreamReconfiguration {
1479 pub fn new() -> Self {
1481 Self::default()
1482 }
1483
1484 pub fn with_storage_class(self, storage_class: StorageClass) -> Self {
1486 Self {
1487 storage_class: Maybe::Specified(Some(storage_class)),
1488 ..self
1489 }
1490 }
1491
1492 pub fn with_retention_policy(self, retention_policy: RetentionPolicy) -> Self {
1494 Self {
1495 retention_policy: Maybe::Specified(Some(retention_policy)),
1496 ..self
1497 }
1498 }
1499
1500 pub fn with_timestamping(self, timestamping: TimestampingReconfiguration) -> Self {
1502 Self {
1503 timestamping: Maybe::Specified(Some(timestamping)),
1504 ..self
1505 }
1506 }
1507
1508 pub fn with_delete_on_empty(self, delete_on_empty: DeleteOnEmptyReconfiguration) -> Self {
1510 Self {
1511 delete_on_empty: Maybe::Specified(Some(delete_on_empty)),
1512 ..self
1513 }
1514 }
1515}
1516
1517impl From<StreamReconfiguration> for api::config::StreamReconfiguration {
1518 fn from(value: StreamReconfiguration) -> Self {
1519 Self {
1520 storage_class: value.storage_class.map(|m| m.map(Into::into)),
1521 retention_policy: value.retention_policy.map(|m| m.map(Into::into)),
1522 timestamping: value.timestamping.map(|m| m.map(Into::into)),
1523 delete_on_empty: value.delete_on_empty.map(|m| m.map(Into::into)),
1524 }
1525 }
1526}
1527
1528#[derive(Debug, Clone, Default)]
1529#[non_exhaustive]
1530pub struct BasinReconfiguration {
1532 pub default_stream_config: Maybe<Option<StreamReconfiguration>>,
1534 pub stream_cipher: Maybe<Option<EncryptionAlgorithm>>,
1536 pub create_stream_on_append: Maybe<bool>,
1539 pub create_stream_on_read: Maybe<bool>,
1541}
1542
1543impl BasinReconfiguration {
1544 pub fn new() -> Self {
1546 Self::default()
1547 }
1548
1549 pub fn with_default_stream_config(self, config: StreamReconfiguration) -> Self {
1552 Self {
1553 default_stream_config: Maybe::Specified(Some(config)),
1554 ..self
1555 }
1556 }
1557
1558 pub fn with_stream_cipher(self, stream_cipher: EncryptionAlgorithm) -> Self {
1560 Self {
1561 stream_cipher: Maybe::Specified(Some(stream_cipher)),
1562 ..self
1563 }
1564 }
1565
1566 pub fn with_create_stream_on_append(self, create_stream_on_append: bool) -> Self {
1569 Self {
1570 create_stream_on_append: Maybe::Specified(create_stream_on_append),
1571 ..self
1572 }
1573 }
1574
1575 pub fn with_create_stream_on_read(self, create_stream_on_read: bool) -> Self {
1578 Self {
1579 create_stream_on_read: Maybe::Specified(create_stream_on_read),
1580 ..self
1581 }
1582 }
1583}
1584
1585impl From<BasinReconfiguration> for api::config::BasinReconfiguration {
1586 fn from(value: BasinReconfiguration) -> Self {
1587 Self {
1588 default_stream_config: value.default_stream_config.map(|m| m.map(Into::into)),
1589 stream_cipher: value.stream_cipher.map(|m| m.map(Into::into)),
1590 create_stream_on_append: value.create_stream_on_append,
1591 create_stream_on_read: value.create_stream_on_read,
1592 }
1593 }
1594}
1595
1596#[derive(Debug, Clone)]
1597#[non_exhaustive]
1598pub struct ReconfigureBasinInput {
1600 pub name: BasinName,
1602 pub config: BasinReconfiguration,
1604}
1605
1606impl ReconfigureBasinInput {
1607 pub fn new(name: BasinName, config: BasinReconfiguration) -> Self {
1609 Self { name, config }
1610 }
1611}
1612
1613#[derive(Debug, Clone, Default)]
1614#[non_exhaustive]
1615pub struct ListAccessTokensInput {
1617 pub prefix: AccessTokenIdPrefix,
1621 pub start_after: AccessTokenIdStartAfter,
1625 pub limit: Option<usize>,
1629}
1630
1631impl ListAccessTokensInput {
1632 pub fn new() -> Self {
1634 Self::default()
1635 }
1636
1637 pub fn with_prefix(self, prefix: AccessTokenIdPrefix) -> Self {
1639 Self { prefix, ..self }
1640 }
1641
1642 pub fn with_start_after(self, start_after: AccessTokenIdStartAfter) -> Self {
1645 Self {
1646 start_after,
1647 ..self
1648 }
1649 }
1650
1651 pub fn with_limit(self, limit: usize) -> Self {
1653 Self {
1654 limit: Some(limit),
1655 ..self
1656 }
1657 }
1658}
1659
1660impl From<ListAccessTokensInput> for api::access::ListAccessTokensRequest {
1661 fn from(value: ListAccessTokensInput) -> Self {
1662 Self {
1663 prefix: Some(value.prefix),
1664 start_after: Some(value.start_after),
1665 limit: value.limit,
1666 }
1667 }
1668}
1669
1670#[derive(Debug, Clone, Default)]
1671pub struct ListAllAccessTokensInput {
1673 pub prefix: AccessTokenIdPrefix,
1677 pub start_after: AccessTokenIdStartAfter,
1681}
1682
1683impl ListAllAccessTokensInput {
1684 pub fn new() -> Self {
1686 Self::default()
1687 }
1688
1689 pub fn with_prefix(self, prefix: AccessTokenIdPrefix) -> Self {
1691 Self { prefix, ..self }
1692 }
1693
1694 pub fn with_start_after(self, start_after: AccessTokenIdStartAfter) -> Self {
1697 Self {
1698 start_after,
1699 ..self
1700 }
1701 }
1702}
1703
1704#[derive(Debug, Clone, PartialEq, Eq)]
1705#[non_exhaustive]
1706pub struct LocationInfo {
1708 pub name: LocationName,
1710 pub is_private: bool,
1712}
1713
1714impl From<api::location::LocationInfo> for LocationInfo {
1715 fn from(value: api::location::LocationInfo) -> Self {
1716 Self {
1717 name: value.name,
1718 is_private: value.is_private,
1719 }
1720 }
1721}
1722
1723#[derive(Debug, Clone)]
1724#[non_exhaustive]
1725pub struct AccessTokenInfo {
1727 pub id: AccessTokenId,
1729 pub expires_at: Option<S2DateTime>,
1731 pub auto_prefix_streams: bool,
1734 pub scope: AccessTokenScope,
1736}
1737
1738impl TryFrom<api::access::AccessTokenInfo> for AccessTokenInfo {
1739 type Error = ValidationError;
1740
1741 fn try_from(value: api::access::AccessTokenInfo) -> Result<Self, Self::Error> {
1742 let expires_at = value.expires_at.map(S2DateTime::try_from).transpose()?;
1743 Ok(Self {
1744 id: value.id,
1745 expires_at,
1746 auto_prefix_streams: value.auto_prefix_streams,
1747 scope: value.scope.into(),
1748 })
1749 }
1750}
1751
1752#[derive(Debug, Clone)]
1753pub enum BasinMatcher {
1757 None,
1759 Exact(BasinName),
1761 Prefix(BasinNamePrefix),
1763}
1764
1765#[derive(Debug, Clone)]
1766pub enum StreamMatcher {
1770 None,
1772 Exact(StreamName),
1774 Prefix(StreamNamePrefix),
1776}
1777
1778#[derive(Debug, Clone)]
1779pub enum AccessTokenMatcher {
1783 None,
1785 Exact(AccessTokenId),
1787 Prefix(AccessTokenIdPrefix),
1789}
1790
1791#[derive(Debug, Clone, Default)]
1792#[non_exhaustive]
1793pub struct ReadWritePermissions {
1795 pub read: bool,
1799 pub write: bool,
1803}
1804
1805impl ReadWritePermissions {
1806 pub fn new() -> Self {
1808 Self::default()
1809 }
1810
1811 pub fn read_only() -> Self {
1813 Self {
1814 read: true,
1815 write: false,
1816 }
1817 }
1818
1819 pub fn write_only() -> Self {
1821 Self {
1822 read: false,
1823 write: true,
1824 }
1825 }
1826
1827 pub fn read_write() -> Self {
1829 Self {
1830 read: true,
1831 write: true,
1832 }
1833 }
1834}
1835
1836impl From<ReadWritePermissions> for api::access::ReadWritePermissions {
1837 fn from(value: ReadWritePermissions) -> Self {
1838 Self {
1839 read: Some(value.read),
1840 write: Some(value.write),
1841 }
1842 }
1843}
1844
1845impl From<api::access::ReadWritePermissions> for ReadWritePermissions {
1846 fn from(value: api::access::ReadWritePermissions) -> Self {
1847 Self {
1848 read: value.read.unwrap_or_default(),
1849 write: value.write.unwrap_or_default(),
1850 }
1851 }
1852}
1853
1854#[derive(Debug, Clone, Default)]
1855#[non_exhaustive]
1856pub struct OperationGroupPermissions {
1860 pub account: Option<ReadWritePermissions>,
1864 pub basin: Option<ReadWritePermissions>,
1868 pub stream: Option<ReadWritePermissions>,
1872}
1873
1874impl OperationGroupPermissions {
1875 pub fn new() -> Self {
1877 Self::default()
1878 }
1879
1880 pub fn read_only_all() -> Self {
1882 Self {
1883 account: Some(ReadWritePermissions::read_only()),
1884 basin: Some(ReadWritePermissions::read_only()),
1885 stream: Some(ReadWritePermissions::read_only()),
1886 }
1887 }
1888
1889 pub fn write_only_all() -> Self {
1891 Self {
1892 account: Some(ReadWritePermissions::write_only()),
1893 basin: Some(ReadWritePermissions::write_only()),
1894 stream: Some(ReadWritePermissions::write_only()),
1895 }
1896 }
1897
1898 pub fn read_write_all() -> Self {
1900 Self {
1901 account: Some(ReadWritePermissions::read_write()),
1902 basin: Some(ReadWritePermissions::read_write()),
1903 stream: Some(ReadWritePermissions::read_write()),
1904 }
1905 }
1906
1907 pub fn with_account(self, account: ReadWritePermissions) -> Self {
1909 Self {
1910 account: Some(account),
1911 ..self
1912 }
1913 }
1914
1915 pub fn with_basin(self, basin: ReadWritePermissions) -> Self {
1917 Self {
1918 basin: Some(basin),
1919 ..self
1920 }
1921 }
1922
1923 pub fn with_stream(self, stream: ReadWritePermissions) -> Self {
1925 Self {
1926 stream: Some(stream),
1927 ..self
1928 }
1929 }
1930}
1931
1932impl From<OperationGroupPermissions> for api::access::PermittedOperationGroups {
1933 fn from(value: OperationGroupPermissions) -> Self {
1934 Self {
1935 account: value.account.map(Into::into),
1936 basin: value.basin.map(Into::into),
1937 stream: value.stream.map(Into::into),
1938 }
1939 }
1940}
1941
1942impl From<api::access::PermittedOperationGroups> for OperationGroupPermissions {
1943 fn from(value: api::access::PermittedOperationGroups) -> Self {
1944 Self {
1945 account: value.account.map(Into::into),
1946 basin: value.basin.map(Into::into),
1947 stream: value.stream.map(Into::into),
1948 }
1949 }
1950}
1951
1952#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
1953pub enum Operation {
1957 ListBasins,
1959 CreateBasin,
1961 GetBasinConfig,
1963 DeleteBasin,
1965 ReconfigureBasin,
1967 ListAccessTokens,
1969 IssueAccessToken,
1971 RevokeAccessToken,
1973 GetAccountMetrics,
1975 GetBasinMetrics,
1977 GetStreamMetrics,
1979 ListStreams,
1981 CreateStream,
1983 GetStreamConfig,
1985 DeleteStream,
1987 ReconfigureStream,
1989 CheckTail,
1991 Append,
1993 Read,
1995 Trim,
1997 Fence,
1999 ListLocations,
2001 GetDefaultLocation,
2003 SetDefaultLocation,
2005}
2006
2007impl From<Operation> for api::access::Operation {
2008 fn from(value: Operation) -> Self {
2009 match value {
2010 Operation::ListBasins => api::access::Operation::ListBasins,
2011 Operation::CreateBasin => api::access::Operation::CreateBasin,
2012 Operation::DeleteBasin => api::access::Operation::DeleteBasin,
2013 Operation::ReconfigureBasin => api::access::Operation::ReconfigureBasin,
2014 Operation::GetBasinConfig => api::access::Operation::GetBasinConfig,
2015 Operation::IssueAccessToken => api::access::Operation::IssueAccessToken,
2016 Operation::RevokeAccessToken => api::access::Operation::RevokeAccessToken,
2017 Operation::ListAccessTokens => api::access::Operation::ListAccessTokens,
2018 Operation::ListStreams => api::access::Operation::ListStreams,
2019 Operation::CreateStream => api::access::Operation::CreateStream,
2020 Operation::DeleteStream => api::access::Operation::DeleteStream,
2021 Operation::GetStreamConfig => api::access::Operation::GetStreamConfig,
2022 Operation::ReconfigureStream => api::access::Operation::ReconfigureStream,
2023 Operation::CheckTail => api::access::Operation::CheckTail,
2024 Operation::Append => api::access::Operation::Append,
2025 Operation::Read => api::access::Operation::Read,
2026 Operation::Trim => api::access::Operation::Trim,
2027 Operation::Fence => api::access::Operation::Fence,
2028 Operation::GetAccountMetrics => api::access::Operation::AccountMetrics,
2029 Operation::GetBasinMetrics => api::access::Operation::BasinMetrics,
2030 Operation::GetStreamMetrics => api::access::Operation::StreamMetrics,
2031 Operation::ListLocations => api::access::Operation::ListLocations,
2032 Operation::GetDefaultLocation => api::access::Operation::GetDefaultLocation,
2033 Operation::SetDefaultLocation => api::access::Operation::SetDefaultLocation,
2034 }
2035 }
2036}
2037
2038impl From<api::access::Operation> for Operation {
2039 fn from(value: api::access::Operation) -> Self {
2040 match value {
2041 api::access::Operation::ListBasins => Operation::ListBasins,
2042 api::access::Operation::CreateBasin => Operation::CreateBasin,
2043 api::access::Operation::DeleteBasin => Operation::DeleteBasin,
2044 api::access::Operation::ReconfigureBasin => Operation::ReconfigureBasin,
2045 api::access::Operation::GetBasinConfig => Operation::GetBasinConfig,
2046 api::access::Operation::IssueAccessToken => Operation::IssueAccessToken,
2047 api::access::Operation::RevokeAccessToken => Operation::RevokeAccessToken,
2048 api::access::Operation::ListAccessTokens => Operation::ListAccessTokens,
2049 api::access::Operation::ListStreams => Operation::ListStreams,
2050 api::access::Operation::CreateStream => Operation::CreateStream,
2051 api::access::Operation::DeleteStream => Operation::DeleteStream,
2052 api::access::Operation::GetStreamConfig => Operation::GetStreamConfig,
2053 api::access::Operation::ReconfigureStream => Operation::ReconfigureStream,
2054 api::access::Operation::CheckTail => Operation::CheckTail,
2055 api::access::Operation::Append => Operation::Append,
2056 api::access::Operation::Read => Operation::Read,
2057 api::access::Operation::Trim => Operation::Trim,
2058 api::access::Operation::Fence => Operation::Fence,
2059 api::access::Operation::AccountMetrics => Operation::GetAccountMetrics,
2060 api::access::Operation::BasinMetrics => Operation::GetBasinMetrics,
2061 api::access::Operation::StreamMetrics => Operation::GetStreamMetrics,
2062 api::access::Operation::ListLocations => Operation::ListLocations,
2063 api::access::Operation::GetDefaultLocation => Operation::GetDefaultLocation,
2064 api::access::Operation::SetDefaultLocation => Operation::SetDefaultLocation,
2065 }
2066 }
2067}
2068
2069#[derive(Debug, Clone)]
2070#[non_exhaustive]
2071pub struct AccessTokenScopeInput {
2079 basins: Option<BasinMatcher>,
2080 streams: Option<StreamMatcher>,
2081 access_tokens: Option<AccessTokenMatcher>,
2082 op_group_perms: Option<OperationGroupPermissions>,
2083 ops: HashSet<Operation>,
2084}
2085
2086impl AccessTokenScopeInput {
2087 pub fn from_ops(ops: impl IntoIterator<Item = Operation>) -> Self {
2089 Self {
2090 basins: None,
2091 streams: None,
2092 access_tokens: None,
2093 op_group_perms: None,
2094 ops: ops.into_iter().collect(),
2095 }
2096 }
2097
2098 pub fn from_op_group_perms(op_group_perms: OperationGroupPermissions) -> Self {
2100 Self {
2101 basins: None,
2102 streams: None,
2103 access_tokens: None,
2104 op_group_perms: Some(op_group_perms),
2105 ops: HashSet::default(),
2106 }
2107 }
2108
2109 pub fn with_ops(self, ops: impl IntoIterator<Item = Operation>) -> Self {
2111 Self {
2112 ops: ops.into_iter().collect(),
2113 ..self
2114 }
2115 }
2116
2117 pub fn with_op_group_perms(self, op_group_perms: OperationGroupPermissions) -> Self {
2119 Self {
2120 op_group_perms: Some(op_group_perms),
2121 ..self
2122 }
2123 }
2124
2125 pub fn with_basins(self, basins: BasinMatcher) -> Self {
2129 Self {
2130 basins: Some(basins),
2131 ..self
2132 }
2133 }
2134
2135 pub fn with_streams(self, streams: StreamMatcher) -> Self {
2139 Self {
2140 streams: Some(streams),
2141 ..self
2142 }
2143 }
2144
2145 pub fn with_access_tokens(self, access_tokens: AccessTokenMatcher) -> Self {
2149 Self {
2150 access_tokens: Some(access_tokens),
2151 ..self
2152 }
2153 }
2154}
2155
2156#[derive(Debug, Clone)]
2157#[non_exhaustive]
2158pub struct AccessTokenScope {
2160 pub basins: Option<BasinMatcher>,
2162 pub streams: Option<StreamMatcher>,
2164 pub access_tokens: Option<AccessTokenMatcher>,
2166 pub op_group_perms: Option<OperationGroupPermissions>,
2168 pub ops: HashSet<Operation>,
2170}
2171
2172impl From<api::access::AccessTokenScope> for AccessTokenScope {
2173 fn from(value: api::access::AccessTokenScope) -> Self {
2174 Self {
2175 basins: value.basins.map(|rs| match rs {
2176 api::access::ResourceSet::Exact(api::access::MaybeEmpty::NonEmpty(e)) => {
2177 BasinMatcher::Exact(e)
2178 }
2179 api::access::ResourceSet::Exact(api::access::MaybeEmpty::Empty) => {
2180 BasinMatcher::None
2181 }
2182 api::access::ResourceSet::Prefix(p) => BasinMatcher::Prefix(p),
2183 }),
2184 streams: value.streams.map(|rs| match rs {
2185 api::access::ResourceSet::Exact(api::access::MaybeEmpty::NonEmpty(e)) => {
2186 StreamMatcher::Exact(e)
2187 }
2188 api::access::ResourceSet::Exact(api::access::MaybeEmpty::Empty) => {
2189 StreamMatcher::None
2190 }
2191 api::access::ResourceSet::Prefix(p) => StreamMatcher::Prefix(p),
2192 }),
2193 access_tokens: value.access_tokens.map(|rs| match rs {
2194 api::access::ResourceSet::Exact(api::access::MaybeEmpty::NonEmpty(e)) => {
2195 AccessTokenMatcher::Exact(e)
2196 }
2197 api::access::ResourceSet::Exact(api::access::MaybeEmpty::Empty) => {
2198 AccessTokenMatcher::None
2199 }
2200 api::access::ResourceSet::Prefix(p) => AccessTokenMatcher::Prefix(p),
2201 }),
2202 op_group_perms: value.op_groups.map(Into::into),
2203 ops: value
2204 .ops
2205 .map(|ops| ops.into_iter().map(Into::into).collect())
2206 .unwrap_or_default(),
2207 }
2208 }
2209}
2210
2211impl From<AccessTokenScopeInput> for api::access::AccessTokenScope {
2212 fn from(value: AccessTokenScopeInput) -> Self {
2213 Self {
2214 basins: value.basins.map(|rs| match rs {
2215 BasinMatcher::None => {
2216 api::access::ResourceSet::Exact(api::access::MaybeEmpty::Empty)
2217 }
2218 BasinMatcher::Exact(e) => {
2219 api::access::ResourceSet::Exact(api::access::MaybeEmpty::NonEmpty(e))
2220 }
2221 BasinMatcher::Prefix(p) => api::access::ResourceSet::Prefix(p),
2222 }),
2223 streams: value.streams.map(|rs| match rs {
2224 StreamMatcher::None => {
2225 api::access::ResourceSet::Exact(api::access::MaybeEmpty::Empty)
2226 }
2227 StreamMatcher::Exact(e) => {
2228 api::access::ResourceSet::Exact(api::access::MaybeEmpty::NonEmpty(e))
2229 }
2230 StreamMatcher::Prefix(p) => api::access::ResourceSet::Prefix(p),
2231 }),
2232 access_tokens: value.access_tokens.map(|rs| match rs {
2233 AccessTokenMatcher::None => {
2234 api::access::ResourceSet::Exact(api::access::MaybeEmpty::Empty)
2235 }
2236 AccessTokenMatcher::Exact(e) => {
2237 api::access::ResourceSet::Exact(api::access::MaybeEmpty::NonEmpty(e))
2238 }
2239 AccessTokenMatcher::Prefix(p) => api::access::ResourceSet::Prefix(p),
2240 }),
2241 op_groups: value.op_group_perms.map(Into::into),
2242 ops: if value.ops.is_empty() {
2243 None
2244 } else {
2245 Some(value.ops.into_iter().map(Into::into).collect())
2246 },
2247 }
2248 }
2249}
2250
2251#[derive(Debug, Clone)]
2252#[non_exhaustive]
2253pub struct IssueAccessTokenInput {
2255 pub id: AccessTokenId,
2257 pub expires_at: Option<S2DateTime>,
2262 pub auto_prefix_streams: bool,
2270 pub scope: AccessTokenScopeInput,
2272}
2273
2274impl IssueAccessTokenInput {
2275 pub fn new(id: AccessTokenId, scope: AccessTokenScopeInput) -> Self {
2277 Self {
2278 id,
2279 expires_at: None,
2280 auto_prefix_streams: false,
2281 scope,
2282 }
2283 }
2284
2285 pub fn with_expires_at(self, expires_at: S2DateTime) -> Self {
2287 Self {
2288 expires_at: Some(expires_at),
2289 ..self
2290 }
2291 }
2292
2293 pub fn with_auto_prefix_streams(self, auto_prefix_streams: bool) -> Self {
2296 Self {
2297 auto_prefix_streams,
2298 ..self
2299 }
2300 }
2301}
2302
2303impl From<IssueAccessTokenInput> for api::access::IssueAccessTokenRequest {
2304 fn from(value: IssueAccessTokenInput) -> Self {
2305 Self {
2306 id: value.id,
2307 expires_at: value.expires_at.map(Into::into),
2308 auto_prefix_streams: value.auto_prefix_streams.then_some(true),
2309 scope: value.scope.into(),
2310 }
2311 }
2312}
2313
2314#[derive(Debug, Clone, Copy, PartialEq, Eq)]
2315pub enum TimeseriesInterval {
2317 Minute,
2319 Hour,
2321 Day,
2323}
2324
2325impl From<TimeseriesInterval> for api::metrics::TimeseriesInterval {
2326 fn from(value: TimeseriesInterval) -> Self {
2327 match value {
2328 TimeseriesInterval::Minute => api::metrics::TimeseriesInterval::Minute,
2329 TimeseriesInterval::Hour => api::metrics::TimeseriesInterval::Hour,
2330 TimeseriesInterval::Day => api::metrics::TimeseriesInterval::Day,
2331 }
2332 }
2333}
2334
2335impl From<api::metrics::TimeseriesInterval> for TimeseriesInterval {
2336 fn from(value: api::metrics::TimeseriesInterval) -> Self {
2337 match value {
2338 api::metrics::TimeseriesInterval::Minute => TimeseriesInterval::Minute,
2339 api::metrics::TimeseriesInterval::Hour => TimeseriesInterval::Hour,
2340 api::metrics::TimeseriesInterval::Day => TimeseriesInterval::Day,
2341 }
2342 }
2343}
2344
2345#[derive(Debug, Clone, Copy)]
2346#[non_exhaustive]
2347pub struct TimeRange {
2349 pub start: u32,
2351 pub end: u32,
2353}
2354
2355impl TimeRange {
2356 pub fn new(start: u32, end: u32) -> Self {
2358 Self { start, end }
2359 }
2360}
2361
2362#[derive(Debug, Clone, Copy)]
2363#[non_exhaustive]
2364pub struct TimeRangeAndInterval {
2366 pub start: u32,
2368 pub end: u32,
2370 pub interval: Option<TimeseriesInterval>,
2374}
2375
2376impl TimeRangeAndInterval {
2377 pub fn new(start: u32, end: u32) -> Self {
2379 Self {
2380 start,
2381 end,
2382 interval: None,
2383 }
2384 }
2385
2386 pub fn with_interval(self, interval: TimeseriesInterval) -> Self {
2388 Self {
2389 interval: Some(interval),
2390 ..self
2391 }
2392 }
2393}
2394
2395#[derive(Debug, Clone, Copy)]
2396pub enum AccountMetricSet {
2398 ActiveBasins(TimeRange),
2401 AccountOps(TimeRangeAndInterval),
2408}
2409
2410#[derive(Debug, Clone)]
2411#[non_exhaustive]
2412pub struct GetAccountMetricsInput {
2414 pub set: AccountMetricSet,
2416}
2417
2418impl GetAccountMetricsInput {
2419 pub fn new(set: AccountMetricSet) -> Self {
2421 Self { set }
2422 }
2423}
2424
2425impl From<GetAccountMetricsInput> for api::metrics::AccountMetricSetRequest {
2426 fn from(value: GetAccountMetricsInput) -> Self {
2427 let (set, start, end, interval) = match value.set {
2428 AccountMetricSet::ActiveBasins(args) => (
2429 api::metrics::AccountMetricSet::ActiveBasins,
2430 args.start,
2431 args.end,
2432 None,
2433 ),
2434 AccountMetricSet::AccountOps(args) => (
2435 api::metrics::AccountMetricSet::AccountOps,
2436 args.start,
2437 args.end,
2438 args.interval,
2439 ),
2440 };
2441 Self {
2442 set,
2443 start: Some(start),
2444 end: Some(end),
2445 interval: interval.map(Into::into),
2446 }
2447 }
2448}
2449
2450#[derive(Debug, Clone, Copy)]
2451pub enum BasinMetricSet {
2453 Storage(TimeRange),
2456 AppendOps(TimeRangeAndInterval),
2464 ReadOps(TimeRangeAndInterval),
2472 ReadThroughput(TimeRangeAndInterval),
2479 AppendThroughput(TimeRangeAndInterval),
2486 BasinOps(TimeRangeAndInterval),
2493}
2494
2495#[derive(Debug, Clone)]
2496#[non_exhaustive]
2497pub struct GetBasinMetricsInput {
2499 pub name: BasinName,
2501 pub set: BasinMetricSet,
2503}
2504
2505impl GetBasinMetricsInput {
2506 pub fn new(name: BasinName, set: BasinMetricSet) -> Self {
2508 Self { name, set }
2509 }
2510}
2511
2512impl From<GetBasinMetricsInput> for (BasinName, api::metrics::BasinMetricSetRequest) {
2513 fn from(value: GetBasinMetricsInput) -> Self {
2514 let (set, start, end, interval) = match value.set {
2515 BasinMetricSet::Storage(args) => (
2516 api::metrics::BasinMetricSet::Storage,
2517 args.start,
2518 args.end,
2519 None,
2520 ),
2521 BasinMetricSet::AppendOps(args) => (
2522 api::metrics::BasinMetricSet::AppendOps,
2523 args.start,
2524 args.end,
2525 args.interval,
2526 ),
2527 BasinMetricSet::ReadOps(args) => (
2528 api::metrics::BasinMetricSet::ReadOps,
2529 args.start,
2530 args.end,
2531 args.interval,
2532 ),
2533 BasinMetricSet::ReadThroughput(args) => (
2534 api::metrics::BasinMetricSet::ReadThroughput,
2535 args.start,
2536 args.end,
2537 args.interval,
2538 ),
2539 BasinMetricSet::AppendThroughput(args) => (
2540 api::metrics::BasinMetricSet::AppendThroughput,
2541 args.start,
2542 args.end,
2543 args.interval,
2544 ),
2545 BasinMetricSet::BasinOps(args) => (
2546 api::metrics::BasinMetricSet::BasinOps,
2547 args.start,
2548 args.end,
2549 args.interval,
2550 ),
2551 };
2552 (
2553 value.name,
2554 api::metrics::BasinMetricSetRequest {
2555 set,
2556 start: Some(start),
2557 end: Some(end),
2558 interval: interval.map(Into::into),
2559 },
2560 )
2561 }
2562}
2563
2564#[derive(Debug, Clone, Copy)]
2565pub enum StreamMetricSet {
2567 Storage(TimeRange),
2570}
2571
2572#[derive(Debug, Clone)]
2573#[non_exhaustive]
2574pub struct GetStreamMetricsInput {
2576 pub basin_name: BasinName,
2578 pub stream_name: StreamName,
2580 pub set: StreamMetricSet,
2582}
2583
2584impl GetStreamMetricsInput {
2585 pub fn new(basin_name: BasinName, stream_name: StreamName, set: StreamMetricSet) -> Self {
2588 Self {
2589 basin_name,
2590 stream_name,
2591 set,
2592 }
2593 }
2594}
2595
2596impl From<GetStreamMetricsInput> for (BasinName, StreamName, api::metrics::StreamMetricSetRequest) {
2597 fn from(value: GetStreamMetricsInput) -> Self {
2598 let (set, start, end, interval) = match value.set {
2599 StreamMetricSet::Storage(args) => (
2600 api::metrics::StreamMetricSet::Storage,
2601 args.start,
2602 args.end,
2603 None,
2604 ),
2605 };
2606 (
2607 value.basin_name,
2608 value.stream_name,
2609 api::metrics::StreamMetricSetRequest {
2610 set,
2611 start: Some(start),
2612 end: Some(end),
2613 interval,
2614 },
2615 )
2616 }
2617}
2618
2619#[derive(Debug, Clone, Copy, PartialEq, Eq)]
2620pub enum MetricUnit {
2622 Bytes,
2624 Operations,
2626}
2627
2628impl From<api::metrics::MetricUnit> for MetricUnit {
2629 fn from(value: api::metrics::MetricUnit) -> Self {
2630 match value {
2631 api::metrics::MetricUnit::Bytes => MetricUnit::Bytes,
2632 api::metrics::MetricUnit::Operations => MetricUnit::Operations,
2633 }
2634 }
2635}
2636
2637#[derive(Debug, Clone)]
2638#[non_exhaustive]
2639pub struct ScalarMetric {
2641 pub name: String,
2643 pub unit: MetricUnit,
2645 pub value: f64,
2647}
2648
2649#[derive(Debug, Clone)]
2650#[non_exhaustive]
2651pub struct AccumulationMetric {
2654 pub name: String,
2656 pub unit: MetricUnit,
2658 pub interval: TimeseriesInterval,
2660 pub values: Vec<(u32, f64)>,
2664}
2665
2666#[derive(Debug, Clone)]
2667#[non_exhaustive]
2668pub struct GaugeMetric {
2670 pub name: String,
2672 pub unit: MetricUnit,
2674 pub values: Vec<(u32, f64)>,
2677}
2678
2679#[derive(Debug, Clone)]
2680#[non_exhaustive]
2681pub struct LabelMetric {
2683 pub name: String,
2685 pub values: Vec<String>,
2687}
2688
2689#[derive(Debug, Clone)]
2690pub enum Metric {
2692 Scalar(ScalarMetric),
2694 Accumulation(AccumulationMetric),
2697 Gauge(GaugeMetric),
2699 Label(LabelMetric),
2701}
2702
2703impl From<api::metrics::Metric> for Metric {
2704 fn from(value: api::metrics::Metric) -> Self {
2705 match value {
2706 api::metrics::Metric::Scalar(sm) => Metric::Scalar(ScalarMetric {
2707 name: sm.name.into(),
2708 unit: sm.unit.into(),
2709 value: sm.value,
2710 }),
2711 api::metrics::Metric::Accumulation(am) => Metric::Accumulation(AccumulationMetric {
2712 name: am.name.into(),
2713 unit: am.unit.into(),
2714 interval: am.interval.into(),
2715 values: am.values,
2716 }),
2717 api::metrics::Metric::Gauge(gm) => Metric::Gauge(GaugeMetric {
2718 name: gm.name.into(),
2719 unit: gm.unit.into(),
2720 values: gm.values,
2721 }),
2722 api::metrics::Metric::Label(lm) => Metric::Label(LabelMetric {
2723 name: lm.name.into(),
2724 values: lm.values,
2725 }),
2726 }
2727 }
2728}
2729
2730#[derive(Debug, Clone, Default)]
2731#[non_exhaustive]
2732pub struct ListStreamsInput {
2734 pub prefix: StreamNamePrefix,
2738 pub start_after: StreamNameStartAfter,
2742 pub limit: Option<usize>,
2746}
2747
2748impl ListStreamsInput {
2749 pub fn new() -> Self {
2751 Self::default()
2752 }
2753
2754 pub fn with_prefix(self, prefix: StreamNamePrefix) -> Self {
2756 Self { prefix, ..self }
2757 }
2758
2759 pub fn with_start_after(self, start_after: StreamNameStartAfter) -> Self {
2762 Self {
2763 start_after,
2764 ..self
2765 }
2766 }
2767
2768 pub fn with_limit(self, limit: usize) -> Self {
2770 Self {
2771 limit: Some(limit),
2772 ..self
2773 }
2774 }
2775}
2776
2777impl From<ListStreamsInput> for api::stream::ListStreamsRequest {
2778 fn from(value: ListStreamsInput) -> Self {
2779 Self {
2780 prefix: Some(value.prefix),
2781 start_after: Some(value.start_after),
2782 limit: value.limit,
2783 }
2784 }
2785}
2786
2787#[derive(Debug, Clone, Default)]
2788pub struct ListAllStreamsInput {
2790 pub prefix: StreamNamePrefix,
2794 pub start_after: StreamNameStartAfter,
2798 pub include_deleted: bool,
2802}
2803
2804impl ListAllStreamsInput {
2805 pub fn new() -> Self {
2807 Self::default()
2808 }
2809
2810 pub fn with_prefix(self, prefix: StreamNamePrefix) -> Self {
2812 Self { prefix, ..self }
2813 }
2814
2815 pub fn with_start_after(self, start_after: StreamNameStartAfter) -> Self {
2818 Self {
2819 start_after,
2820 ..self
2821 }
2822 }
2823
2824 pub fn with_include_deleted(self, include_deleted: bool) -> Self {
2826 Self {
2827 include_deleted,
2828 ..self
2829 }
2830 }
2831}
2832
2833#[derive(Debug, Clone, PartialEq, Eq)]
2834#[non_exhaustive]
2835pub struct StreamInfo {
2837 pub name: StreamName,
2839 pub created_at: S2DateTime,
2841 pub deleted_at: Option<S2DateTime>,
2843 pub cipher: Option<EncryptionAlgorithm>,
2845}
2846
2847impl TryFrom<api::stream::StreamInfo> for StreamInfo {
2848 type Error = ValidationError;
2849
2850 fn try_from(value: api::stream::StreamInfo) -> Result<Self, Self::Error> {
2851 Ok(Self {
2852 name: value.name,
2853 created_at: value.created_at.try_into()?,
2854 deleted_at: value.deleted_at.map(S2DateTime::try_from).transpose()?,
2855 cipher: value.cipher.map(Into::into),
2856 })
2857 }
2858}
2859
2860#[derive(Debug, Clone)]
2861#[non_exhaustive]
2862pub struct CreateStreamInput {
2864 pub name: StreamName,
2866 pub config: Option<StreamConfig>,
2870 idempotency_token: String,
2871}
2872
2873impl CreateStreamInput {
2874 pub fn new(name: StreamName) -> Self {
2876 Self {
2877 name,
2878 config: None,
2879 idempotency_token: idempotency_token(),
2880 }
2881 }
2882
2883 pub fn with_config(self, config: StreamConfig) -> Self {
2885 Self {
2886 config: Some(config),
2887 ..self
2888 }
2889 }
2890}
2891
2892impl From<CreateStreamInput> for (api::stream::CreateStreamRequest, String) {
2893 fn from(value: CreateStreamInput) -> Self {
2894 (
2895 api::stream::CreateStreamRequest {
2896 stream: value.name,
2897 config: value.config.map(Into::into),
2898 },
2899 value.idempotency_token,
2900 )
2901 }
2902}
2903
2904#[derive(Debug, Clone)]
2905#[non_exhaustive]
2906pub struct EnsureStreamInput {
2909 pub name: StreamName,
2911 pub config: Option<StreamConfig>,
2915}
2916
2917impl EnsureStreamInput {
2918 pub fn new(name: StreamName) -> Self {
2920 Self { name, config: None }
2921 }
2922
2923 pub fn with_config(self, config: StreamConfig) -> Self {
2925 Self {
2926 config: Some(config),
2927 ..self
2928 }
2929 }
2930}
2931
2932impl From<EnsureStreamInput> for (StreamName, Option<api::config::StreamConfig>) {
2933 fn from(value: EnsureStreamInput) -> Self {
2934 (value.name, value.config.map(Into::into))
2935 }
2936}
2937
2938#[derive(Debug, Clone)]
2939#[non_exhaustive]
2940pub struct DeleteStreamInput {
2942 pub name: StreamName,
2944 pub ignore_not_found: bool,
2946}
2947
2948impl DeleteStreamInput {
2949 pub fn new(name: StreamName) -> Self {
2951 Self {
2952 name,
2953 ignore_not_found: false,
2954 }
2955 }
2956
2957 pub fn with_ignore_not_found(self, ignore_not_found: bool) -> Self {
2959 Self {
2960 ignore_not_found,
2961 ..self
2962 }
2963 }
2964}
2965
2966#[derive(Debug, Clone)]
2967#[non_exhaustive]
2968pub struct ReconfigureStreamInput {
2970 pub name: StreamName,
2972 pub config: StreamReconfiguration,
2974}
2975
2976impl ReconfigureStreamInput {
2977 pub fn new(name: StreamName, config: StreamReconfiguration) -> Self {
2979 Self { name, config }
2980 }
2981}
2982
2983#[derive(Debug, Clone, PartialEq, Eq)]
2984pub struct FencingToken(String);
2990
2991impl FencingToken {
2992 pub(crate) fn from_server(value: String) -> Self {
2993 Self(value)
2994 }
2995
2996 pub fn generate(n: usize) -> Result<Self, ValidationError> {
2998 rand::rng()
2999 .sample_iter(&rand::distr::Alphanumeric)
3000 .take(n)
3001 .map(char::from)
3002 .collect::<String>()
3003 .parse()
3004 }
3005}
3006
3007impl FromStr for FencingToken {
3008 type Err = ValidationError;
3009
3010 fn from_str(s: &str) -> Result<Self, Self::Err> {
3011 if s.len() > MAX_FENCING_TOKEN_LENGTH {
3012 return Err(ValidationError(format!(
3013 "fencing token exceeds {MAX_FENCING_TOKEN_LENGTH} bytes in length",
3014 )));
3015 }
3016 Ok(FencingToken(s.to_string()))
3017 }
3018}
3019
3020impl std::fmt::Display for FencingToken {
3021 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
3022 write!(f, "{}", self.0)
3023 }
3024}
3025
3026impl Deref for FencingToken {
3027 type Target = str;
3028
3029 fn deref(&self) -> &Self::Target {
3030 &self.0
3031 }
3032}
3033
3034#[derive(Debug, Clone, Copy, PartialEq)]
3035#[non_exhaustive]
3036pub struct StreamPosition {
3038 pub seq_num: u64,
3040 pub timestamp: u64,
3043}
3044
3045impl StreamPosition {
3046 pub fn new(seq_num: u64, timestamp: u64) -> Self {
3050 Self { seq_num, timestamp }
3051 }
3052}
3053
3054impl std::fmt::Display for StreamPosition {
3055 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
3056 write!(f, "seq_num={}, timestamp={}", self.seq_num, self.timestamp)
3057 }
3058}
3059
3060impl From<api::stream::proto::StreamPosition> for StreamPosition {
3061 fn from(value: api::stream::proto::StreamPosition) -> Self {
3062 Self {
3063 seq_num: value.seq_num,
3064 timestamp: value.timestamp,
3065 }
3066 }
3067}
3068
3069impl From<api::stream::StreamPosition> for StreamPosition {
3070 fn from(value: api::stream::StreamPosition) -> Self {
3071 Self {
3072 seq_num: value.seq_num,
3073 timestamp: value.timestamp,
3074 }
3075 }
3076}
3077
3078#[derive(Debug, Clone, PartialEq)]
3079#[non_exhaustive]
3080pub struct Header {
3082 pub name: Bytes,
3084 pub value: Bytes,
3086}
3087
3088impl Header {
3089 pub fn new(name: impl Into<Bytes>, value: impl Into<Bytes>) -> Self {
3091 Self {
3092 name: name.into(),
3093 value: value.into(),
3094 }
3095 }
3096}
3097
3098impl From<Header> for api::stream::proto::Header {
3099 fn from(value: Header) -> Self {
3100 Self {
3101 name: value.name,
3102 value: value.value,
3103 }
3104 }
3105}
3106
3107impl From<api::stream::proto::Header> for Header {
3108 fn from(value: api::stream::proto::Header) -> Self {
3109 Self {
3110 name: value.name,
3111 value: value.value,
3112 }
3113 }
3114}
3115
3116#[derive(Debug, Clone, PartialEq)]
3117pub struct AppendRecord {
3119 body: Bytes,
3120 headers: Vec<Header>,
3121 timestamp: Option<u64>,
3122}
3123
3124impl AppendRecord {
3125 fn validate(self) -> Result<Self, ValidationError> {
3126 if self.metered_bytes() > RECORD_BATCH_MAX.bytes {
3127 Err(ValidationError(format!(
3128 "metered_bytes: {} exceeds {}",
3129 self.metered_bytes(),
3130 RECORD_BATCH_MAX.bytes
3131 )))
3132 } else {
3133 Ok(self)
3134 }
3135 }
3136
3137 pub fn new(body: impl Into<Bytes>) -> Result<Self, ValidationError> {
3139 let record = Self {
3140 body: body.into(),
3141 headers: Vec::default(),
3142 timestamp: None,
3143 };
3144 record.validate()
3145 }
3146
3147 pub fn with_headers(
3149 self,
3150 headers: impl IntoIterator<Item = Header>,
3151 ) -> Result<Self, ValidationError> {
3152 let record = Self {
3153 headers: headers.into_iter().collect(),
3154 ..self
3155 };
3156 record.validate()
3157 }
3158
3159 pub fn with_timestamp(self, timestamp: u64) -> Self {
3163 Self {
3164 timestamp: Some(timestamp),
3165 ..self
3166 }
3167 }
3168
3169 pub fn body(&self) -> &[u8] {
3171 &self.body
3172 }
3173
3174 pub fn headers(&self) -> &[Header] {
3176 &self.headers
3177 }
3178
3179 pub fn timestamp(&self) -> Option<u64> {
3181 self.timestamp
3182 }
3183}
3184
3185impl From<AppendRecord> for api::stream::proto::AppendRecord {
3186 fn from(value: AppendRecord) -> Self {
3187 Self {
3188 timestamp: value.timestamp,
3189 headers: value.headers.into_iter().map(Into::into).collect(),
3190 body: value.body,
3191 }
3192 }
3193}
3194
3195pub trait MeteredBytes {
3202 fn metered_bytes(&self) -> usize;
3204}
3205
3206macro_rules! metered_bytes_impl {
3207 ($ty:ty) => {
3208 impl MeteredBytes for $ty {
3209 fn metered_bytes(&self) -> usize {
3210 8 + (2 * self.headers.len())
3211 + self
3212 .headers
3213 .iter()
3214 .map(|h| h.name.len() + h.value.len())
3215 .sum::<usize>()
3216 + self.body.len()
3217 }
3218 }
3219 };
3220}
3221
3222metered_bytes_impl!(AppendRecord);
3223
3224impl MeteredSize for AppendRecord {
3225 fn metered_size(&self) -> usize {
3226 self.metered_bytes()
3227 }
3228}
3229
3230#[derive(Debug, Clone)]
3231pub struct AppendRecordBatch(Metered<Vec<AppendRecord>>);
3240
3241impl From<Metered<Vec<AppendRecord>>> for AppendRecordBatch {
3242 fn from(records: Metered<Vec<AppendRecord>>) -> Self {
3243 Self(records)
3244 }
3245}
3246
3247impl AppendRecordBatch {
3248 pub fn try_from_iter<I>(iter: I) -> Result<Self, ValidationError>
3250 where
3251 I: IntoIterator<Item = AppendRecord>,
3252 {
3253 let mut records = Metered::with_capacity(RECORD_BATCH_MAX.count);
3254
3255 for record in iter {
3256 records.push(Metered::from(record));
3257
3258 if records.metered_size() > RECORD_BATCH_MAX.bytes {
3259 return Err(ValidationError(format!(
3260 "batch size in metered bytes ({}) exceeds {}",
3261 records.metered_size(),
3262 RECORD_BATCH_MAX.bytes
3263 )));
3264 }
3265
3266 if records.len() > RECORD_BATCH_MAX.count {
3267 return Err(ValidationError(format!(
3268 "number of records in the batch exceeds {}",
3269 RECORD_BATCH_MAX.count
3270 )));
3271 }
3272 }
3273
3274 if records.is_empty() {
3275 return Err(ValidationError("batch is empty".into()));
3276 }
3277
3278 Ok(records.into())
3279 }
3280}
3281
3282impl Deref for AppendRecordBatch {
3283 type Target = [AppendRecord];
3284
3285 fn deref(&self) -> &Self::Target {
3286 &self.0[..]
3287 }
3288}
3289
3290impl MeteredBytes for AppendRecordBatch {
3291 fn metered_bytes(&self) -> usize {
3292 self.0.metered_size()
3293 }
3294}
3295
3296impl IntoIterator for AppendRecordBatch {
3297 type Item = AppendRecord;
3298 type IntoIter = std::vec::IntoIter<AppendRecord>;
3299
3300 fn into_iter(self) -> Self::IntoIter {
3301 self.0.into_iter()
3302 }
3303}
3304
3305impl<'a> IntoIterator for &'a AppendRecordBatch {
3306 type Item = &'a AppendRecord;
3307 type IntoIter = std::slice::Iter<'a, AppendRecord>;
3308
3309 fn into_iter(self) -> Self::IntoIter {
3310 self.0.iter()
3311 }
3312}
3313
3314#[derive(Debug, Clone)]
3315pub enum Command {
3317 Fence {
3319 fencing_token: FencingToken,
3321 },
3322 Trim {
3324 trim_point: u64,
3326 },
3327}
3328
3329#[derive(Debug, Clone)]
3330#[non_exhaustive]
3331pub struct CommandRecord {
3335 pub command: Command,
3337 pub timestamp: Option<u64>,
3339}
3340
3341impl CommandRecord {
3342 const FENCE: &[u8] = b"fence";
3343 const TRIM: &[u8] = b"trim";
3344
3345 pub fn fence(fencing_token: FencingToken) -> Self {
3350 Self {
3351 command: Command::Fence { fencing_token },
3352 timestamp: None,
3353 }
3354 }
3355
3356 pub fn trim(trim_point: u64) -> Self {
3363 Self {
3364 command: Command::Trim { trim_point },
3365 timestamp: None,
3366 }
3367 }
3368
3369 pub fn with_timestamp(self, timestamp: u64) -> Self {
3371 Self {
3372 timestamp: Some(timestamp),
3373 ..self
3374 }
3375 }
3376}
3377
3378impl From<CommandRecord> for AppendRecord {
3379 fn from(value: CommandRecord) -> Self {
3380 let (header_value, body) = match value.command {
3381 Command::Fence { fencing_token } => (
3382 CommandRecord::FENCE,
3383 Bytes::copy_from_slice(fencing_token.as_bytes()),
3384 ),
3385 Command::Trim { trim_point } => (
3386 CommandRecord::TRIM,
3387 Bytes::copy_from_slice(&trim_point.to_be_bytes()),
3388 ),
3389 };
3390 Self {
3391 body,
3392 headers: vec![Header::new("", header_value)],
3393 timestamp: value.timestamp,
3394 }
3395 }
3396}
3397
3398#[derive(Debug, Clone)]
3399#[non_exhaustive]
3400pub struct AppendInput {
3403 pub records: AppendRecordBatch,
3405 pub match_seq_num: Option<u64>,
3409 pub fencing_token: Option<FencingToken>,
3414 pub stream_config: Option<StreamConfig>,
3424}
3425
3426impl AppendInput {
3427 pub fn new(records: AppendRecordBatch) -> Self {
3429 Self {
3430 records,
3431 match_seq_num: None,
3432 fencing_token: None,
3433 stream_config: None,
3434 }
3435 }
3436
3437 pub fn with_stream_config(self, stream_config: StreamConfig) -> Self {
3439 Self {
3440 stream_config: Some(stream_config),
3441 ..self
3442 }
3443 }
3444
3445 pub fn with_match_seq_num(self, match_seq_num: u64) -> Self {
3447 Self {
3448 match_seq_num: Some(match_seq_num),
3449 ..self
3450 }
3451 }
3452
3453 pub fn with_fencing_token(self, fencing_token: FencingToken) -> Self {
3455 Self {
3456 fencing_token: Some(fencing_token),
3457 ..self
3458 }
3459 }
3460}
3461
3462impl From<AppendInput> for api::stream::proto::AppendInput {
3463 fn from(value: AppendInput) -> Self {
3464 Self {
3465 records: value.records.iter().cloned().map(Into::into).collect(),
3466 match_seq_num: value.match_seq_num,
3467 fencing_token: value.fencing_token.map(|t| t.to_string()),
3468 }
3469 }
3470}
3471
3472#[derive(Debug, Clone, PartialEq)]
3473#[non_exhaustive]
3474pub struct AppendAck {
3476 pub start: StreamPosition,
3478 pub end: StreamPosition,
3484 pub tail: StreamPosition,
3489}
3490
3491impl AppendAck {
3492 pub fn new(start: StreamPosition, end: StreamPosition, tail: StreamPosition) -> Self {
3496 Self { start, end, tail }
3497 }
3498}
3499
3500impl From<api::stream::proto::AppendAck> for AppendAck {
3501 fn from(value: api::stream::proto::AppendAck) -> Self {
3502 Self {
3503 start: value.start.unwrap_or_default().into(),
3504 end: value.end.unwrap_or_default().into(),
3505 tail: value.tail.unwrap_or_default().into(),
3506 }
3507 }
3508}
3509
3510#[derive(Debug, Clone, Copy)]
3511pub enum ReadFrom {
3513 SeqNum(u64),
3515 Timestamp(u64),
3517 TailOffset(u64),
3519}
3520
3521impl Default for ReadFrom {
3522 fn default() -> Self {
3523 Self::SeqNum(0)
3524 }
3525}
3526
3527#[derive(Debug, Default, Clone)]
3528#[non_exhaustive]
3529pub struct ReadStart {
3531 pub from: ReadFrom,
3535 pub clamp_to_tail: bool,
3539}
3540
3541impl ReadStart {
3542 pub fn new() -> Self {
3544 Self::default()
3545 }
3546
3547 pub fn with_from(self, from: ReadFrom) -> Self {
3549 Self { from, ..self }
3550 }
3551
3552 pub fn with_clamp_to_tail(self, clamp_to_tail: bool) -> Self {
3554 Self {
3555 clamp_to_tail,
3556 ..self
3557 }
3558 }
3559}
3560
3561impl From<ReadStart> for api::stream::ReadStart {
3562 fn from(value: ReadStart) -> Self {
3563 let (seq_num, timestamp, tail_offset) = match value.from {
3564 ReadFrom::SeqNum(n) => (Some(n), None, None),
3565 ReadFrom::Timestamp(t) => (None, Some(t), None),
3566 ReadFrom::TailOffset(o) => (None, None, Some(o)),
3567 };
3568 Self {
3569 seq_num,
3570 timestamp,
3571 tail_offset,
3572 clamp: if value.clamp_to_tail {
3573 Some(true)
3574 } else {
3575 None
3576 },
3577 }
3578 }
3579}
3580
3581#[derive(Debug, Clone, Default)]
3582#[non_exhaustive]
3583pub struct ReadLimits {
3585 pub count: Option<usize>,
3589 pub bytes: Option<usize>,
3593}
3594
3595impl ReadLimits {
3596 pub fn new() -> Self {
3598 Self::default()
3599 }
3600
3601 pub fn with_count(self, count: usize) -> Self {
3603 Self {
3604 count: Some(count),
3605 ..self
3606 }
3607 }
3608
3609 pub fn with_bytes(self, bytes: usize) -> Self {
3611 Self {
3612 bytes: Some(bytes),
3613 ..self
3614 }
3615 }
3616}
3617
3618#[derive(Debug, Clone, Default)]
3619#[non_exhaustive]
3620pub struct ReadStop {
3622 pub limits: ReadLimits,
3626 pub until: Option<RangeTo<u64>>,
3630 pub wait: Option<u32>,
3640}
3641
3642impl ReadStop {
3643 pub fn new() -> Self {
3645 Self::default()
3646 }
3647
3648 pub fn with_limits(self, limits: ReadLimits) -> Self {
3650 Self { limits, ..self }
3651 }
3652
3653 pub fn with_until(self, until: RangeTo<u64>) -> Self {
3655 Self {
3656 until: Some(until),
3657 ..self
3658 }
3659 }
3660
3661 pub fn with_wait(self, wait: u32) -> Self {
3663 Self {
3664 wait: Some(wait),
3665 ..self
3666 }
3667 }
3668}
3669
3670impl From<ReadStop> for api::stream::ReadEnd {
3671 fn from(value: ReadStop) -> Self {
3672 Self {
3673 count: value.limits.count,
3674 bytes: value.limits.bytes,
3675 until: value.until.map(|r| r.end),
3676 wait: value.wait,
3677 }
3678 }
3679}
3680
3681#[derive(Debug, Clone, Default)]
3682#[non_exhaustive]
3683pub struct ReadInput {
3686 pub start: ReadStart,
3690 pub stop: ReadStop,
3694 pub ignore_command_records: bool,
3698 pub stream_config: Option<StreamConfig>,
3703}
3704
3705#[derive(Debug, Clone, Copy, Default, Eq, PartialEq)]
3706#[non_exhaustive]
3707pub enum ReadSessionRetryPolicy {
3709 #[default]
3711 Budgeted,
3712 Indefinite,
3718}
3719
3720#[derive(Debug, Clone, Default)]
3721#[non_exhaustive]
3722pub struct ReadSessionConfig {
3724 pub retry_policy: ReadSessionRetryPolicy,
3730}
3731
3732impl ReadSessionConfig {
3733 pub fn new() -> Self {
3735 Self::default()
3736 }
3737
3738 pub fn with_retry_policy(self, retry_policy: ReadSessionRetryPolicy) -> Self {
3740 Self {
3741 retry_policy,
3742 ..self
3743 }
3744 }
3745}
3746
3747impl ReadInput {
3748 pub fn new() -> Self {
3750 Self::default()
3751 }
3752
3753 pub fn with_start(self, start: ReadStart) -> Self {
3755 Self { start, ..self }
3756 }
3757
3758 pub fn with_stop(self, stop: ReadStop) -> Self {
3760 Self { stop, ..self }
3761 }
3762
3763 pub fn with_ignore_command_records(self, ignore_command_records: bool) -> Self {
3765 Self {
3766 ignore_command_records,
3767 ..self
3768 }
3769 }
3770
3771 pub fn with_stream_config(self, stream_config: StreamConfig) -> Self {
3773 Self {
3774 stream_config: Some(stream_config),
3775 ..self
3776 }
3777 }
3778}
3779
3780#[derive(Debug, Clone)]
3781#[non_exhaustive]
3782pub struct SequencedRecord {
3784 pub seq_num: u64,
3786 pub body: Bytes,
3788 pub headers: Vec<Header>,
3790 pub timestamp: u64,
3792}
3793
3794impl SequencedRecord {
3795 pub fn from_parts(
3799 seq_num: u64,
3800 timestamp: u64,
3801 headers: Vec<Header>,
3802 body: impl Into<Bytes>,
3803 ) -> Self {
3804 Self {
3805 seq_num,
3806 timestamp,
3807 body: body.into(),
3808 headers,
3809 }
3810 }
3811
3812 pub fn is_command_record(&self) -> bool {
3814 self.headers.len() == 1 && *self.headers[0].name == *b""
3815 }
3816}
3817
3818impl From<api::stream::proto::SequencedRecord> for SequencedRecord {
3819 fn from(value: api::stream::proto::SequencedRecord) -> Self {
3820 Self {
3821 seq_num: value.seq_num,
3822 body: value.body,
3823 headers: value.headers.into_iter().map(Into::into).collect(),
3824 timestamp: value.timestamp,
3825 }
3826 }
3827}
3828
3829metered_bytes_impl!(SequencedRecord);
3830
3831#[derive(Debug, Clone)]
3832#[non_exhaustive]
3833pub struct ReadBatch {
3836 pub records: Vec<SequencedRecord>,
3843 pub tail: Option<StreamPosition>,
3848}
3849
3850impl ReadBatch {
3851 pub fn new(records: Vec<SequencedRecord>, tail: Option<StreamPosition>) -> Self {
3855 Self { records, tail }
3856 }
3857
3858 pub(crate) fn from_api(batch: api::stream::proto::ReadBatch) -> Self {
3859 Self {
3860 records: batch.records.into_iter().map(Into::into).collect(),
3861 tail: batch.tail.map(Into::into),
3862 }
3863 }
3864}
3865
3866pub type Streaming<T> = Pin<Box<dyn Send + futures_core::Stream<Item = Result<T, RequestError>>>>;
3868
3869fn idempotency_token() -> String {
3870 uuid::Uuid::new_v4().simple().to_string()
3871}
3872
3873#[cfg(test)]
3874mod tests {
3875 use proptest::prelude::*;
3876 use rstest::rstest;
3877
3878 use super::*;
3879
3880 type HeaderParts = (Vec<u8>, Vec<u8>);
3881 type AppendRecordParts = (Vec<u8>, Vec<HeaderParts>);
3882
3883 fn byte_vec_strategy(max_len: usize) -> impl Strategy<Value = Vec<u8>> {
3884 prop::collection::vec(any::<u8>(), 0..=max_len)
3885 }
3886
3887 fn header_parts_strategy() -> impl Strategy<Value = HeaderParts> {
3888 (byte_vec_strategy(32), byte_vec_strategy(64))
3889 }
3890
3891 fn string_strategy(max_chars: usize) -> impl Strategy<Value = String> {
3892 prop::collection::vec(any::<char>(), 0..=max_chars)
3893 .prop_map(|chars| chars.into_iter().collect())
3894 }
3895
3896 fn read_from_strategy() -> impl Strategy<Value = ReadFrom> {
3897 prop_oneof![
3898 any::<u64>().prop_map(ReadFrom::SeqNum),
3899 any::<u64>().prop_map(ReadFrom::Timestamp),
3900 any::<u64>().prop_map(ReadFrom::TailOffset),
3901 ]
3902 }
3903
3904 fn append_record_parts_strategy() -> impl Strategy<Value = AppendRecordParts> {
3905 (
3906 byte_vec_strategy(256),
3907 prop::collection::vec(header_parts_strategy(), 0..=16),
3908 )
3909 }
3910
3911 fn proto_stream_position_strategy() -> impl Strategy<Value = api::stream::proto::StreamPosition>
3912 {
3913 (any::<u64>(), any::<u64>()).prop_map(|(seq_num, timestamp)| {
3914 api::stream::proto::StreamPosition { seq_num, timestamp }
3915 })
3916 }
3917
3918 fn headers_from_parts(headers: &[HeaderParts]) -> Vec<Header> {
3919 headers
3920 .iter()
3921 .map(|(name, value)| Header::new(Bytes::from(name.clone()), Bytes::from(value.clone())))
3922 .collect()
3923 }
3924
3925 fn expected_metered_bytes(body: &[u8], headers: &[HeaderParts]) -> usize {
3926 8 + (2 * headers.len())
3927 + headers
3928 .iter()
3929 .map(|(name, value)| name.len() + value.len())
3930 .sum::<usize>()
3931 + body.len()
3932 }
3933
3934 #[test]
3937 fn s2_datetime_parse_valid_rfc3339() {
3938 let dt: S2DateTime = "2024-01-15T12:30:00Z".parse().unwrap();
3939 assert_eq!(dt.to_string(), "2024-01-15T12:30:00Z");
3940 }
3941
3942 #[test]
3943 fn s2_datetime_parse_with_offset() {
3944 let dt: S2DateTime = "2024-06-01T08:00:00+05:30".parse().unwrap();
3945 assert_eq!(dt.to_string(), "2024-06-01T08:00:00+05:30");
3946
3947 let offset_dt: time::OffsetDateTime = dt.into();
3948 assert_eq!(
3949 offset_dt.offset(),
3950 time::UtcOffset::from_hms(5, 30, 0).unwrap()
3951 );
3952 }
3953
3954 #[test]
3955 fn s2_datetime_parse_invalid() {
3956 let err = "not-a-date".parse::<S2DateTime>();
3957 assert!(err.is_err());
3958 }
3959
3960 #[test]
3961 fn s2_datetime_roundtrip_via_offset_datetime() {
3962 let odt = time::OffsetDateTime::from_unix_timestamp(1_700_000_000).unwrap();
3963 let dt = S2DateTime::try_from(odt).unwrap();
3964 let back: time::OffsetDateTime = dt.into();
3965 assert_eq!(odt, back);
3966 }
3967
3968 #[rstest]
3971 #[case::https_with_scheme("https://aws.s2.dev", Scheme::HTTPS)]
3972 #[case::http_with_scheme("http://localhost:8080", Scheme::HTTP)]
3973 #[case::default_https("aws.s2.dev", Scheme::HTTPS)]
3974 fn account_endpoint_parse(#[case] input: &str, #[case] expected_scheme: Scheme) {
3975 let ep: AccountEndpoint = input.parse().unwrap();
3976 assert_eq!(ep.scheme, expected_scheme);
3977 }
3978
3979 #[rstest]
3982 #[case::https_parent_zone("https://{basin}.b.s2.dev", Scheme::HTTPS, true)]
3983 #[case::http_direct("http://localhost:8080", Scheme::HTTP, false)]
3984 #[case::default_https_parent_zone("{basin}.b.s2.dev", Scheme::HTTPS, true)]
3985 fn basin_endpoint_parse(
3986 #[case] input: &str,
3987 #[case] expected_scheme: Scheme,
3988 #[case] expected_parent_zone: bool,
3989 ) {
3990 let ep: BasinEndpoint = input.parse().unwrap();
3991 assert_eq!(ep.scheme, expected_scheme);
3992 assert_eq!(
3993 matches!(ep.authority, BasinAuthority::ParentZone(_)),
3994 expected_parent_zone
3995 );
3996 }
3997
3998 #[test]
4001 fn s2_endpoints_new_requires_same_scheme() {
4002 let account: AccountEndpoint = "https://aws.s2.dev".parse().unwrap();
4003 let basin: BasinEndpoint = "http://localhost:8080".parse().unwrap();
4004 let err = S2Endpoints::new(account, basin);
4005 assert!(err.is_err());
4006 }
4007
4008 #[test]
4009 fn s2_endpoints_new_same_scheme_succeeds() {
4010 let account: AccountEndpoint = "https://aws.s2.dev".parse().unwrap();
4011 let basin: BasinEndpoint = "https://{basin}.b.s2.dev".parse().unwrap();
4012 let ep = S2Endpoints::new(account, basin).unwrap();
4013 assert_eq!(ep.scheme, Scheme::HTTPS);
4014 }
4015
4016 #[test]
4017 fn s2_endpoints_for_endpoint_defaults_to_https() {
4018 let ep = S2Endpoints::for_endpoint("localhost:8080").unwrap();
4019 let authority: Authority = "localhost:8080".parse().unwrap();
4020 assert_eq!(ep.scheme, Scheme::HTTPS);
4021 assert_eq!(ep.account_authority, authority);
4022 assert_eq!(ep.basin_authority, BasinAuthority::Direct(authority));
4023 }
4024
4025 #[test]
4026 fn s2_endpoints_for_endpoint_accepts_explicit_scheme() {
4027 let ep = S2Endpoints::for_endpoint("http://localhost:8080").unwrap();
4028 assert_eq!(ep.scheme, Scheme::HTTP);
4029 }
4030
4031 #[test]
4032 fn s2_endpoints_for_endpoint_rejects_invalid_endpoint() {
4033 assert!(S2Endpoints::for_endpoint("not a valid endpoint").is_err());
4034 }
4035
4036 #[rstest]
4039 #[case::none(Compression::None, CompressionAlgorithm::None)]
4040 #[case::gzip(Compression::Gzip, CompressionAlgorithm::Gzip)]
4041 #[case::zstd(Compression::Zstd, CompressionAlgorithm::Zstd)]
4042 fn compression_conversion(#[case] sdk: Compression, #[case] api: CompressionAlgorithm) {
4043 assert_eq!(CompressionAlgorithm::from(sdk), api);
4044 }
4045
4046 #[test]
4049 fn retry_config_defaults() {
4050 let rc = RetryConfig::default();
4051 assert_eq!(rc.max_attempts.get(), 3);
4052 assert_eq!(rc.min_base_delay, Duration::from_millis(100));
4053 assert_eq!(rc.max_base_delay, Duration::from_secs(1));
4054 assert!(matches!(rc.append_retry_policy, AppendRetryPolicy::All));
4055 }
4056
4057 #[test]
4058 fn retry_config_max_retries() {
4059 let rc = RetryConfig::default();
4060 assert_eq!(rc.max_retries(), 2);
4061 }
4062
4063 #[test]
4066 fn s2_config_defaults() {
4067 let cfg = S2Config::new("test-token");
4068 assert_eq!(cfg.connection_timeout, Duration::from_secs(3));
4069 assert_eq!(cfg.request_timeout, Duration::from_secs(5));
4070 assert!(!cfg.insecure_skip_cert_verification);
4071 }
4072
4073 #[cfg(feature = "_hidden")]
4074 #[rstest]
4075 #[case::matching_compression("content-encoding", "gzip", Compression::Gzip)]
4076 #[case::mixed_case("Content-Encoding", "identity", Compression::None)]
4077 #[case::empty_value("content-encoding", "", Compression::None)]
4078 fn default_headers_reject_content_encoding(
4079 #[case] name: &str,
4080 #[case] value: &str,
4081 #[case] compression: Compression,
4082 ) {
4083 let headers = HeaderMap::from_iter([(
4084 name.parse::<http::header::HeaderName>().unwrap(),
4085 HeaderValue::from_str(value).unwrap(),
4086 )]);
4087 let error = S2Config::new("token")
4088 .with_compression(compression)
4089 .with_default_headers(headers)
4090 .unwrap_err();
4091 assert!(error.0.contains("Content-Encoding"));
4092 assert!(error.0.contains("with_compression"));
4093 }
4094
4095 #[cfg(feature = "_hidden")]
4096 #[rstest]
4097 #[case::content_length("content-length", "123")]
4098 #[case::content_length_mixed_case("Content-Length", "0")]
4099 #[case::content_length_empty("content-length", "")]
4100 #[case::transfer_encoding("transfer-encoding", "chunked")]
4101 #[case::transfer_encoding_mixed_case("Transfer-Encoding", "chunked")]
4102 #[case::transfer_encoding_empty("transfer-encoding", "")]
4103 fn default_headers_reject_framing_headers(#[case] name: &str, #[case] value: &str) {
4104 let headers = HeaderMap::from_iter([(
4105 name.parse::<http::header::HeaderName>().unwrap(),
4106 HeaderValue::from_str(value).unwrap(),
4107 )]);
4108 let error = S2Config::new("token")
4109 .with_default_headers(headers)
4110 .unwrap_err();
4111 assert!(error.0.contains(&name.to_ascii_lowercase()));
4112 assert!(error.0.contains("framing"));
4113 }
4114
4115 #[rstest]
4118 #[case::standard(StorageClass::Standard)]
4119 #[case::express(StorageClass::Express)]
4120 fn storage_class_roundtrip(#[case] sdk: StorageClass) {
4121 let api: api::config::StorageClass = sdk.into();
4122 let back: StorageClass = api.into();
4123 assert_eq!(back, sdk);
4124 }
4125
4126 #[rstest]
4129 #[case::age(RetentionPolicy::Age(3600))]
4130 #[case::infinite(RetentionPolicy::Infinite)]
4131 fn retention_policy_roundtrip(#[case] sdk: RetentionPolicy) {
4132 let api: api::config::RetentionPolicy = sdk.into();
4133 let back: RetentionPolicy = api.into();
4134 assert_eq!(back, sdk);
4135 }
4136
4137 #[rstest]
4140 #[case::client_prefer(
4141 TimestampingMode::ClientPrefer,
4142 api::config::TimestampingMode::ClientPrefer
4143 )]
4144 #[case::client_require(
4145 TimestampingMode::ClientRequire,
4146 api::config::TimestampingMode::ClientRequire
4147 )]
4148 #[case::arrival(TimestampingMode::Arrival, api::config::TimestampingMode::Arrival)]
4149 fn timestamping_mode_roundtrip(
4150 #[case] sdk: TimestampingMode,
4151 #[case] expected_api: api::config::TimestampingMode,
4152 ) {
4153 let converted: api::config::TimestampingMode = sdk.into();
4154 assert_eq!(converted, expected_api);
4155 let back: TimestampingMode = converted.into();
4156 assert_eq!(back, sdk);
4157 }
4158
4159 #[test]
4162 fn timestamping_config_roundtrip() {
4163 let sdk = TimestampingConfig {
4164 mode: Some(TimestampingMode::Arrival),
4165 uncapped: Some(true),
4166 };
4167 let api: api::config::TimestampingConfig = sdk.into();
4168 let back: TimestampingConfig = api.into();
4169 assert_eq!(back, sdk);
4170 }
4171
4172 #[test]
4175 fn delete_on_empty_config_roundtrip() {
4176 let sdk = DeleteOnEmptyConfig::new().with_min_age(Duration::from_secs(300));
4177 let api: api::config::DeleteOnEmptyConfig = sdk.into();
4178 let back: DeleteOnEmptyConfig = api.into();
4179 assert_eq!(back, sdk);
4180 }
4181
4182 #[test]
4185 fn stream_config_builder_and_roundtrip() {
4186 let sdk = StreamConfig::new()
4187 .with_storage_class(StorageClass::Express)
4188 .with_retention_policy(RetentionPolicy::Age(86400))
4189 .with_timestamping(TimestampingConfig {
4190 mode: Some(TimestampingMode::ClientPrefer),
4191 uncapped: None,
4192 })
4193 .with_delete_on_empty(DeleteOnEmptyConfig { min_age_secs: 60 });
4194 let api: api::config::StreamConfig = sdk.clone().into();
4195 let back: StreamConfig = api.into();
4196 assert_eq!(back, sdk);
4197 }
4198
4199 #[test]
4202 fn basin_config_builder_and_roundtrip() {
4203 let sdk = BasinConfig::new()
4204 .with_default_stream_config(
4205 StreamConfig::new().with_storage_class(StorageClass::Standard),
4206 )
4207 .with_create_stream_on_append(true)
4208 .with_create_stream_on_read(false);
4209 let api: api::config::BasinConfig = sdk.clone().into();
4210 let back: BasinConfig = api.into();
4211 assert_eq!(back, sdk);
4212 }
4213
4214 proptest! {
4217 #[test]
4218 fn fencing_token_parse_accepts_only_within_byte_limit(
4219 token in string_strategy(MAX_FENCING_TOKEN_LENGTH + 8),
4220 ) {
4221 let parsed = token.parse::<FencingToken>();
4222
4223 if token.len() <= MAX_FENCING_TOKEN_LENGTH {
4224 prop_assert_eq!(parsed.unwrap().to_string(), token);
4225 } else {
4226 prop_assert!(parsed.is_err());
4227 }
4228 }
4229 }
4230
4231 #[test]
4234 fn stream_position_display() {
4235 let pos = StreamPosition {
4236 seq_num: 42,
4237 timestamp: 1700000000,
4238 };
4239 assert_eq!(pos.to_string(), "seq_num=42, timestamp=1700000000");
4240 }
4241
4242 proptest! {
4243 #[test]
4244 fn stream_position_conversions_preserve_values(seq_num in any::<u64>(), timestamp in any::<u64>()) {
4245 let proto: StreamPosition = api::stream::proto::StreamPosition {
4246 seq_num,
4247 timestamp,
4248 }
4249 .into();
4250 prop_assert_eq!(proto.seq_num, seq_num);
4251 prop_assert_eq!(proto.timestamp, timestamp);
4252
4253 let api: StreamPosition = api::stream::StreamPosition {
4254 seq_num,
4255 timestamp,
4256 }
4257 .into();
4258 prop_assert_eq!(api.seq_num, seq_num);
4259 prop_assert_eq!(api.timestamp, timestamp);
4260 }
4261 }
4262
4263 proptest! {
4266 #[test]
4267 fn header_proto_roundtrip_preserves_binary_parts(
4268 name in byte_vec_strategy(64),
4269 value in byte_vec_strategy(128),
4270 ) {
4271 let header = Header::new(Bytes::from(name.clone()), Bytes::from(value.clone()));
4272 let proto: api::stream::proto::Header = header.into();
4273 let back: Header = proto.into();
4274
4275 prop_assert_eq!(back.name.as_ref(), name.as_slice());
4276 prop_assert_eq!(back.value.as_ref(), value.as_slice());
4277 }
4278 }
4279
4280 #[test]
4283 fn append_record_too_large() {
4284 let big_body = vec![0u8; RECORD_BATCH_MAX.bytes + 1];
4285 assert!(AppendRecord::new(big_body).is_err());
4286 }
4287
4288 proptest! {
4291 #[test]
4292 fn append_record_preserves_fields_and_metered_byte_formula(
4293 (body, headers) in append_record_parts_strategy(),
4294 timestamp in proptest::option::of(any::<u64>()),
4295 ) {
4296 let mut record = AppendRecord::new(body.clone())
4297 .unwrap()
4298 .with_headers(headers_from_parts(&headers))
4299 .unwrap();
4300 if let Some(timestamp) = timestamp {
4301 record = record.with_timestamp(timestamp);
4302 }
4303
4304 prop_assert_eq!(record.body(), body.as_slice());
4305 prop_assert_eq!(record.headers().len(), headers.len());
4306 prop_assert_eq!(record.timestamp(), timestamp);
4307 prop_assert_eq!(record.metered_bytes(), expected_metered_bytes(&body, &headers));
4308
4309 for (actual, (expected_name, expected_value)) in record.headers().iter().zip(headers.iter()) {
4310 prop_assert_eq!(actual.name.as_ref(), expected_name.as_slice());
4311 prop_assert_eq!(actual.value.as_ref(), expected_value.as_slice());
4312 }
4313 }
4314 }
4315
4316 #[test]
4319 fn append_record_batch_empty_is_err() {
4320 let result = AppendRecordBatch::try_from_iter(vec![]);
4321 assert!(result.is_err());
4322 }
4323
4324 #[test]
4325 fn append_record_batch_too_many_records() {
4326 let records: Vec<_> = (0..1001).map(|_| AppendRecord::new("x").unwrap()).collect();
4327 let result = AppendRecordBatch::try_from_iter(records);
4328 assert!(result.is_err());
4329 }
4330
4331 proptest! {
4332 #[test]
4333 fn append_record_batch_metered_bytes_is_sum_of_records(
4334 records in prop::collection::vec(append_record_parts_strategy(), 1..=32),
4335 ) {
4336 let expected = records
4337 .iter()
4338 .map(|(body, headers)| expected_metered_bytes(body, headers))
4339 .sum::<usize>();
4340 let records = records
4341 .into_iter()
4342 .map(|(body, headers)| {
4343 AppendRecord::new(body)
4344 .unwrap()
4345 .with_headers(headers_from_parts(&headers))
4346 .unwrap()
4347 })
4348 .collect::<Vec<_>>();
4349
4350 let batch = AppendRecordBatch::try_from_iter(records).unwrap();
4351 prop_assert_eq!(batch.metered_bytes(), expected);
4352 prop_assert_eq!(batch.iter().map(MeteredBytes::metered_bytes).sum::<usize>(), expected);
4353 }
4354 }
4355
4356 #[test]
4359 fn command_record_fence() {
4360 let token: FencingToken = "tok".parse().unwrap();
4361 let cmd = CommandRecord::fence(token);
4362 let record: AppendRecord = cmd.into();
4363 assert_eq!(record.headers().len(), 1);
4364 assert_eq!(record.headers()[0].name.as_ref(), b"");
4365 assert_eq!(record.headers()[0].value.as_ref(), b"fence");
4366 assert_eq!(record.body(), b"tok");
4367 }
4368
4369 #[test]
4370 fn command_record_trim() {
4371 let cmd = CommandRecord::trim(42);
4372 let record: AppendRecord = cmd.into();
4373 assert_eq!(record.headers().len(), 1);
4374 assert_eq!(record.headers()[0].value.as_ref(), b"trim");
4375 assert_eq!(record.body(), &42u64.to_be_bytes());
4376 }
4377
4378 #[rstest]
4381 #[case::command(vec![Header::new("", "fence")], true)]
4382 #[case::regular(vec![Header::new("key", "value")], false)]
4383 #[case::no_headers(vec![], false)]
4384 fn sequenced_record_command_detection(#[case] headers: Vec<Header>, #[case] expected: bool) {
4385 let record = SequencedRecord {
4386 seq_num: 0,
4387 body: Bytes::from("data"),
4388 headers,
4389 timestamp: 0,
4390 };
4391 assert_eq!(record.is_command_record(), expected);
4392 }
4393
4394 proptest! {
4397 #[test]
4398 fn read_start_to_api_sets_only_selected_position_field(
4399 from in read_from_strategy(),
4400 clamp_to_tail in any::<bool>(),
4401 ) {
4402 let (seq_num, timestamp, tail_offset) = match from {
4403 ReadFrom::SeqNum(value) => (Some(value), None, None),
4404 ReadFrom::Timestamp(value) => (None, Some(value), None),
4405 ReadFrom::TailOffset(value) => (None, None, Some(value)),
4406 };
4407 let api: api::stream::ReadStart = ReadStart::new()
4408 .with_from(from)
4409 .with_clamp_to_tail(clamp_to_tail)
4410 .into();
4411
4412 prop_assert_eq!(api.seq_num, seq_num);
4413 prop_assert_eq!(api.timestamp, timestamp);
4414 prop_assert_eq!(api.tail_offset, tail_offset);
4415 prop_assert_eq!(api.clamp, clamp_to_tail.then_some(true));
4416 }
4417 }
4418
4419 #[test]
4422 fn read_stop_to_api() {
4423 let stop = ReadStop::new()
4424 .with_limits(ReadLimits::new().with_count(50))
4425 .with_until(..1000)
4426 .with_wait(30);
4427 let api: api::stream::ReadEnd = stop.into();
4428 assert_eq!(api.count, Some(50));
4429 assert_eq!(api.until, Some(1000));
4430 assert_eq!(api.wait, Some(30));
4431 }
4432
4433 #[test]
4436 fn operation_roundtrip_all_variants() {
4437 let variants = [
4438 Operation::ListBasins,
4439 Operation::CreateBasin,
4440 Operation::GetBasinConfig,
4441 Operation::DeleteBasin,
4442 Operation::ReconfigureBasin,
4443 Operation::ListAccessTokens,
4444 Operation::IssueAccessToken,
4445 Operation::RevokeAccessToken,
4446 Operation::GetAccountMetrics,
4447 Operation::GetBasinMetrics,
4448 Operation::GetStreamMetrics,
4449 Operation::ListStreams,
4450 Operation::CreateStream,
4451 Operation::GetStreamConfig,
4452 Operation::DeleteStream,
4453 Operation::ReconfigureStream,
4454 Operation::CheckTail,
4455 Operation::Append,
4456 Operation::Read,
4457 Operation::Trim,
4458 Operation::Fence,
4459 Operation::ListLocations,
4460 Operation::GetDefaultLocation,
4461 Operation::SetDefaultLocation,
4462 ];
4463 for op in variants {
4464 let api_op: api::access::Operation = op.into();
4465 let back: Operation = api_op.into();
4466 assert_eq!(back, op);
4467 }
4468 }
4469
4470 #[test]
4473 fn metric_unit_conversion() {
4474 assert_eq!(
4475 MetricUnit::from(api::metrics::MetricUnit::Bytes),
4476 MetricUnit::Bytes
4477 );
4478 assert_eq!(
4479 MetricUnit::from(api::metrics::MetricUnit::Operations),
4480 MetricUnit::Operations
4481 );
4482 }
4483
4484 proptest! {
4487 #[test]
4488 fn append_ack_from_proto_preserves_present_positions_and_defaults_missing(
4489 start in proptest::option::of(proto_stream_position_strategy()),
4490 end in proptest::option::of(proto_stream_position_strategy()),
4491 tail in proptest::option::of(proto_stream_position_strategy()),
4492 ) {
4493 let expected_start = start.unwrap_or_default();
4494 let expected_end = end.unwrap_or_default();
4495 let expected_tail = tail.unwrap_or_default();
4496 let ack: AppendAck = api::stream::proto::AppendAck { start, end, tail }.into();
4497
4498 prop_assert_eq!(ack.start.seq_num, expected_start.seq_num);
4499 prop_assert_eq!(ack.start.timestamp, expected_start.timestamp);
4500 prop_assert_eq!(ack.end.seq_num, expected_end.seq_num);
4501 prop_assert_eq!(ack.end.timestamp, expected_end.timestamp);
4502 prop_assert_eq!(ack.tail.seq_num, expected_tail.seq_num);
4503 prop_assert_eq!(ack.tail.timestamp, expected_tail.timestamp);
4504 }
4505 }
4506
4507 #[test]
4510 fn read_batch_from_api() {
4511 let proto_batch = api::stream::proto::ReadBatch {
4512 records: vec![api::stream::proto::SequencedRecord {
4513 seq_num: 0,
4514 body: Bytes::from("hi"),
4515 headers: vec![api::stream::proto::Header {
4516 name: Bytes::from("k"),
4517 value: Bytes::from("v"),
4518 }],
4519 timestamp: 42,
4520 }],
4521 tail: Some(api::stream::proto::StreamPosition {
4522 seq_num: 1,
4523 timestamp: 42,
4524 }),
4525 };
4526 let batch = ReadBatch::from_api(proto_batch);
4527 assert_eq!(batch.records.len(), 1);
4528 assert_eq!(batch.records[0].seq_num, 0);
4529 assert_eq!(batch.records[0].timestamp, 42);
4530 assert_eq!(batch.records[0].body.as_ref(), b"hi");
4531 assert_eq!(batch.records[0].headers.len(), 1);
4532 assert_eq!(batch.records[0].headers[0].name.as_ref(), b"k");
4533 assert_eq!(batch.records[0].headers[0].value.as_ref(), b"v");
4534 assert_eq!(
4535 batch.tail,
4536 Some(StreamPosition {
4537 seq_num: 1,
4538 timestamp: 42,
4539 })
4540 );
4541 }
4542
4543 #[test]
4546 fn create_basin_input_to_api() {
4547 let name: BasinName = "test-basin-name".parse().unwrap();
4548 let input = CreateBasinInput::new(name.clone()).with_config(BasinConfig::new());
4549 let (req, token): (api::basin::CreateBasinRequest, String) = input.into();
4550 assert_eq!(req.basin, name);
4551 assert!(req.config.is_some());
4552 assert!(!token.is_empty());
4553 }
4554
4555 #[test]
4558 fn create_stream_input_to_api() {
4559 let name: StreamName = "my-stream".parse().unwrap();
4560 let input = CreateStreamInput::new(name.clone()).with_config(StreamConfig::new());
4561 let (req, token): (api::stream::CreateStreamRequest, String) = input.into();
4562 assert_eq!(req.stream, name);
4563 assert!(req.config.is_some());
4564 assert!(!token.is_empty());
4565 }
4566
4567 #[test]
4570 fn sequenced_record_from_proto() {
4571 let proto = api::stream::proto::SequencedRecord {
4572 seq_num: 99,
4573 body: Bytes::from("data"),
4574 headers: vec![api::stream::proto::Header {
4575 name: Bytes::from("k"),
4576 value: Bytes::from("v"),
4577 }],
4578 timestamp: 1234,
4579 };
4580 let record: SequencedRecord = proto.into();
4581 assert_eq!(record.seq_num, 99);
4582 assert_eq!(record.body.as_ref(), b"data");
4583 assert_eq!(record.headers.len(), 1);
4584 assert_eq!(record.headers[0].name.as_ref(), b"k");
4585 assert_eq!(record.headers[0].value.as_ref(), b"v");
4586 assert_eq!(record.timestamp, 1234);
4587 }
4588}