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
15use bytes::Bytes;
16use http::{
17 header::HeaderValue,
18 uri::{Authority, Scheme},
19};
20use rand::RngExt;
21use s2_api::{v1 as api, v1::stream::s2s::CompressionAlgorithm};
22pub use s2_common::ValidationError;
24pub use s2_common::access::AccessTokenId;
28pub use s2_common::access::AccessTokenIdPrefix;
30pub use s2_common::access::AccessTokenIdStartAfter;
32pub use s2_common::basin::BasinName;
37pub use s2_common::basin::BasinNamePrefix;
39pub use s2_common::basin::BasinNameStartAfter;
41pub use s2_common::location::LocationName;
46pub use s2_common::stream::StreamName;
50pub use s2_common::stream::StreamNamePrefix;
52pub use s2_common::stream::StreamNameStartAfter;
54pub use s2_common::{
55 caps::RECORD_BATCH_MAX,
56 encryption::{EncryptionAlgorithm, EncryptionKey},
57};
58
59pub(crate) const ONE_MIB: u32 = 1024 * 1024;
60
61use s2_common::{maybe::Maybe, record::MAX_FENCING_TOKEN_LENGTH, resources::ProvisionResult};
62use secrecy::SecretString;
63
64use crate::api::{ApiError, ApiErrorResponse};
65
66#[derive(Debug, Clone, Copy, PartialEq, Eq)]
72pub struct S2DateTime(time::OffsetDateTime);
73
74impl TryFrom<time::OffsetDateTime> for S2DateTime {
75 type Error = ValidationError;
76
77 fn try_from(dt: time::OffsetDateTime) -> Result<Self, Self::Error> {
78 dt.format(&time::format_description::well_known::Rfc3339)
79 .map_err(|e| ValidationError(format!("not a valid RFC 3339 datetime: {e}")))?;
80 Ok(Self(dt))
81 }
82}
83
84impl From<S2DateTime> for time::OffsetDateTime {
85 fn from(dt: S2DateTime) -> Self {
86 dt.0
87 }
88}
89
90impl FromStr for S2DateTime {
91 type Err = ValidationError;
92
93 fn from_str(s: &str) -> Result<Self, Self::Err> {
94 time::OffsetDateTime::parse(s, &time::format_description::well_known::Rfc3339)
95 .map(Self)
96 .map_err(|e| ValidationError(format!("not a valid RFC 3339 datetime: {e}")))
97 }
98}
99
100impl fmt::Display for S2DateTime {
101 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
102 write!(
103 f,
104 "{}",
105 self.0
106 .format(&time::format_description::well_known::Rfc3339)
107 .expect("RFC3339 formatting should not fail for S2DateTime")
108 )
109 }
110}
111
112#[derive(Debug, Clone, PartialEq)]
114pub(crate) enum BasinAuthority {
115 ParentZone(Authority),
117 Direct(Authority),
119}
120
121#[derive(Debug, Clone)]
123pub struct AccountEndpoint {
124 scheme: Scheme,
125 authority: Authority,
126}
127
128impl AccountEndpoint {
129 pub fn new(endpoint: &str) -> Result<Self, ValidationError> {
131 endpoint.parse()
132 }
133}
134
135impl FromStr for AccountEndpoint {
136 type Err = ValidationError;
137
138 fn from_str(s: &str) -> Result<Self, Self::Err> {
139 let (scheme, authority) = match s.find("://") {
140 Some(idx) => {
141 let scheme: Scheme = s[..idx]
142 .parse()
143 .map_err(|_| "invalid account endpoint scheme".to_string())?;
144 (scheme, &s[idx + 3..])
145 }
146 None => (Scheme::HTTPS, s),
147 };
148 Ok(Self {
149 scheme,
150 authority: authority
151 .parse()
152 .map_err(|e| format!("invalid account endpoint authority: {e}"))?,
153 })
154 }
155}
156
157#[derive(Debug, Clone)]
159pub struct BasinEndpoint {
160 scheme: Scheme,
161 authority: BasinAuthority,
162}
163
164impl BasinEndpoint {
165 pub fn new(endpoint: &str) -> Result<Self, ValidationError> {
167 endpoint.parse()
168 }
169}
170
171impl FromStr for BasinEndpoint {
172 type Err = ValidationError;
173
174 fn from_str(s: &str) -> Result<Self, Self::Err> {
175 let (scheme, authority) = match s.find("://") {
176 Some(idx) => {
177 let scheme: Scheme = s[..idx]
178 .parse()
179 .map_err(|_| "invalid basin endpoint scheme".to_string())?;
180 (scheme, &s[idx + 3..])
181 }
182 None => (Scheme::HTTPS, s),
183 };
184 let authority = if let Some(authority) = authority.strip_prefix("{basin}.") {
185 BasinAuthority::ParentZone(
186 authority
187 .parse()
188 .map_err(|e| format!("invalid basin endpoint authority: {e}"))?,
189 )
190 } else {
191 BasinAuthority::Direct(
192 authority
193 .parse()
194 .map_err(|e| format!("invalid basin endpoint authority: {e}"))?,
195 )
196 };
197 Ok(Self { scheme, authority })
198 }
199}
200
201#[derive(Debug, Clone)]
202#[non_exhaustive]
203pub struct S2Endpoints {
205 pub(crate) scheme: Scheme,
206 pub(crate) account_authority: Authority,
207 pub(crate) basin_authority: BasinAuthority,
208}
209
210impl S2Endpoints {
211 pub fn new(
213 account_endpoint: AccountEndpoint,
214 basin_endpoint: BasinEndpoint,
215 ) -> Result<Self, ValidationError> {
216 if account_endpoint.scheme != basin_endpoint.scheme {
217 return Err("account and basin endpoints must have the same scheme".into());
218 }
219 Ok(Self {
220 scheme: account_endpoint.scheme,
221 account_authority: account_endpoint.authority,
222 basin_authority: basin_endpoint.authority,
223 })
224 }
225
226 pub fn from_env() -> Result<Self, ValidationError> {
232 let account_endpoint: AccountEndpoint = match std::env::var("S2_ACCOUNT_ENDPOINT") {
233 Ok(endpoint) => endpoint.parse()?,
234 Err(VarError::NotPresent) => return Err("S2_ACCOUNT_ENDPOINT env var not set".into()),
235 Err(VarError::NotUnicode(_)) => {
236 return Err("S2_ACCOUNT_ENDPOINT is not valid unicode".into());
237 }
238 };
239
240 let basin_endpoint: BasinEndpoint = match std::env::var("S2_BASIN_ENDPOINT") {
241 Ok(endpoint) => endpoint.parse()?,
242 Err(VarError::NotPresent) => return Err("S2_BASIN_ENDPOINT env var not set".into()),
243 Err(VarError::NotUnicode(_)) => {
244 return Err("S2_BASIN_ENDPOINT is not valid unicode".into());
245 }
246 };
247
248 if account_endpoint.scheme != basin_endpoint.scheme {
249 return Err(
250 "S2_ACCOUNT_ENDPOINT and S2_BASIN_ENDPOINT must have the same scheme".into(),
251 );
252 }
253
254 Ok(Self {
255 scheme: account_endpoint.scheme,
256 account_authority: account_endpoint.authority,
257 basin_authority: basin_endpoint.authority,
258 })
259 }
260
261 pub(crate) fn for_aws() -> Self {
262 Self {
263 scheme: Scheme::HTTPS,
264 account_authority: "aws.s2.dev".try_into().expect("valid authority"),
265 basin_authority: BasinAuthority::ParentZone(
266 "b.s2.dev".try_into().expect("valid authority"),
267 ),
268 }
269 }
270}
271
272#[derive(Debug, Clone, Copy)]
273pub enum Compression {
275 None,
277 Gzip,
279 Zstd,
281}
282
283impl From<Compression> for CompressionAlgorithm {
284 fn from(value: Compression) -> Self {
285 match value {
286 Compression::None => CompressionAlgorithm::None,
287 Compression::Gzip => CompressionAlgorithm::Gzip,
288 Compression::Zstd => CompressionAlgorithm::Zstd,
289 }
290 }
291}
292
293#[derive(Debug, Clone, Copy, PartialEq)]
294#[non_exhaustive]
295pub enum AppendRetryPolicy {
298 All,
300 NoSideEffects,
310}
311
312#[derive(Debug, Clone)]
313#[non_exhaustive]
314pub struct RetryConfig {
323 pub max_attempts: NonZeroU32,
327 pub min_base_delay: Duration,
331 pub max_base_delay: Duration,
335 pub append_retry_policy: AppendRetryPolicy,
340}
341
342impl Default for RetryConfig {
343 fn default() -> Self {
344 Self {
345 max_attempts: NonZeroU32::new(3).expect("valid non-zero u32"),
346 min_base_delay: Duration::from_millis(100),
347 max_base_delay: Duration::from_secs(1),
348 append_retry_policy: AppendRetryPolicy::All,
349 }
350 }
351}
352
353impl RetryConfig {
354 pub fn new() -> Self {
356 Self::default()
357 }
358
359 pub(crate) fn max_retries(&self) -> u32 {
360 self.max_attempts.get() - 1
361 }
362
363 pub fn with_max_attempts(self, max_attempts: NonZeroU32) -> Self {
365 Self {
366 max_attempts,
367 ..self
368 }
369 }
370
371 pub fn with_min_base_delay(self, min_base_delay: Duration) -> Self {
373 Self {
374 min_base_delay,
375 ..self
376 }
377 }
378
379 pub fn with_max_base_delay(self, max_base_delay: Duration) -> Self {
381 Self {
382 max_base_delay,
383 ..self
384 }
385 }
386
387 pub fn with_append_retry_policy(self, append_retry_policy: AppendRetryPolicy) -> Self {
390 Self {
391 append_retry_policy,
392 ..self
393 }
394 }
395}
396
397#[derive(Debug, Clone)]
398#[non_exhaustive]
399pub struct S2Config {
401 pub(crate) access_token: SecretString,
402 pub(crate) endpoints: S2Endpoints,
403 pub(crate) connection_timeout: Duration,
404 pub(crate) request_timeout: Duration,
405 pub(crate) retry: RetryConfig,
406 pub(crate) compression: Compression,
407 pub(crate) user_agent: HeaderValue,
408 pub(crate) insecure_skip_cert_verification: bool,
409 pub(crate) rustls_crypto_provider: Option<Arc<rustls::crypto::CryptoProvider>>,
410}
411
412impl S2Config {
413 pub fn new(access_token: impl Into<String>) -> Self {
415 Self {
416 access_token: access_token.into().into(),
417 endpoints: S2Endpoints::for_aws(),
418 connection_timeout: Duration::from_secs(3),
419 request_timeout: Duration::from_secs(5),
420 retry: RetryConfig::new(),
421 compression: Compression::None,
422 user_agent: concat!("s2-sdk-rust/", env!("CARGO_PKG_VERSION"))
423 .parse()
424 .expect("valid user agent"),
425 insecure_skip_cert_verification: false,
426 rustls_crypto_provider: default_rustls_crypto_provider(),
427 }
428 }
429
430 pub fn with_endpoints(self, endpoints: S2Endpoints) -> Self {
432 Self { endpoints, ..self }
433 }
434
435 pub fn with_connection_timeout(self, connection_timeout: Duration) -> Self {
439 Self {
440 connection_timeout,
441 ..self
442 }
443 }
444
445 pub fn with_request_timeout(self, request_timeout: Duration) -> Self {
449 Self {
450 request_timeout,
451 ..self
452 }
453 }
454
455 pub fn with_retry(self, retry: RetryConfig) -> Self {
459 Self { retry, ..self }
460 }
461
462 pub fn with_compression(self, compression: Compression) -> Self {
466 Self {
467 compression,
468 ..self
469 }
470 }
471
472 pub fn with_insecure_skip_cert_verification(self, skip: bool) -> Self {
484 Self {
485 insecure_skip_cert_verification: skip,
486 ..self
487 }
488 }
489
490 pub fn with_rustls_crypto_provider(
500 self,
501 provider: impl Into<Arc<rustls::crypto::CryptoProvider>>,
502 ) -> Self {
503 Self {
504 rustls_crypto_provider: Some(provider.into()),
505 ..self
506 }
507 }
508
509 #[cfg(feature = "rustls-aws-lc-rs")]
513 pub fn with_rustls_aws_lc_rs_crypto_provider(self) -> Self {
514 self.with_rustls_crypto_provider(rustls::crypto::aws_lc_rs::default_provider())
515 }
516
517 #[cfg(feature = "rustls-ring")]
521 pub fn with_rustls_ring_crypto_provider(self) -> Self {
522 self.with_rustls_crypto_provider(rustls::crypto::ring::default_provider())
523 }
524
525 #[doc(hidden)]
526 #[cfg(feature = "_hidden")]
527 pub fn with_user_agent(self, user_agent: impl Into<String>) -> Result<Self, ValidationError> {
528 let user_agent = user_agent
529 .into()
530 .parse()
531 .map_err(|e| ValidationError(format!("invalid user agent: {e}")))?;
532 Ok(Self { user_agent, ..self })
533 }
534}
535
536#[cfg(feature = "rustls-aws-lc-rs")]
537fn default_rustls_crypto_provider() -> Option<Arc<rustls::crypto::CryptoProvider>> {
538 Some(Arc::new(rustls::crypto::aws_lc_rs::default_provider()))
539}
540
541#[cfg(all(not(feature = "rustls-aws-lc-rs"), feature = "rustls-ring"))]
542fn default_rustls_crypto_provider() -> Option<Arc<rustls::crypto::CryptoProvider>> {
543 Some(Arc::new(rustls::crypto::ring::default_provider()))
544}
545
546#[cfg(all(not(feature = "rustls-aws-lc-rs"), not(feature = "rustls-ring")))]
547fn default_rustls_crypto_provider() -> Option<Arc<rustls::crypto::CryptoProvider>> {
548 None
549}
550
551#[derive(Debug, Default, Clone, PartialEq, Eq)]
552#[non_exhaustive]
553pub struct Page<T> {
555 pub values: Vec<T>,
557 pub has_more: bool,
559}
560
561impl<T> Page<T> {
562 pub(crate) fn new(values: impl Into<Vec<T>>, has_more: bool) -> Self {
563 Self {
564 values: values.into(),
565 has_more,
566 }
567 }
568}
569
570#[derive(Debug, Clone, Copy, PartialEq, Eq)]
571pub enum StorageClass {
573 Standard,
575 Express,
577}
578
579impl From<api::config::StorageClass> for StorageClass {
580 fn from(value: api::config::StorageClass) -> Self {
581 match value {
582 api::config::StorageClass::Standard => StorageClass::Standard,
583 api::config::StorageClass::Express => StorageClass::Express,
584 }
585 }
586}
587
588impl From<StorageClass> for api::config::StorageClass {
589 fn from(value: StorageClass) -> Self {
590 match value {
591 StorageClass::Standard => api::config::StorageClass::Standard,
592 StorageClass::Express => api::config::StorageClass::Express,
593 }
594 }
595}
596
597#[derive(Debug, Clone, Copy, PartialEq, Eq)]
598pub enum RetentionPolicy {
600 Age(u64),
602 Infinite,
604}
605
606impl From<api::config::RetentionPolicy> for RetentionPolicy {
607 fn from(value: api::config::RetentionPolicy) -> Self {
608 match value {
609 api::config::RetentionPolicy::Age(secs) => RetentionPolicy::Age(secs),
610 api::config::RetentionPolicy::Infinite(_) => RetentionPolicy::Infinite,
611 }
612 }
613}
614
615impl From<RetentionPolicy> for api::config::RetentionPolicy {
616 fn from(value: RetentionPolicy) -> Self {
617 match value {
618 RetentionPolicy::Age(secs) => api::config::RetentionPolicy::Age(secs),
619 RetentionPolicy::Infinite => {
620 api::config::RetentionPolicy::Infinite(api::config::InfiniteRetention {})
621 }
622 }
623 }
624}
625
626#[derive(Debug, Clone, Copy, PartialEq, Eq)]
627pub enum TimestampingMode {
629 ClientPrefer,
631 ClientRequire,
633 Arrival,
635}
636
637impl From<api::config::TimestampingMode> for TimestampingMode {
638 fn from(value: api::config::TimestampingMode) -> Self {
639 match value {
640 api::config::TimestampingMode::ClientPrefer => TimestampingMode::ClientPrefer,
641 api::config::TimestampingMode::ClientRequire => TimestampingMode::ClientRequire,
642 api::config::TimestampingMode::Arrival => TimestampingMode::Arrival,
643 }
644 }
645}
646
647impl From<TimestampingMode> for api::config::TimestampingMode {
648 fn from(value: TimestampingMode) -> Self {
649 match value {
650 TimestampingMode::ClientPrefer => api::config::TimestampingMode::ClientPrefer,
651 TimestampingMode::ClientRequire => api::config::TimestampingMode::ClientRequire,
652 TimestampingMode::Arrival => api::config::TimestampingMode::Arrival,
653 }
654 }
655}
656
657#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
658#[non_exhaustive]
659pub struct TimestampingConfig {
661 pub mode: Option<TimestampingMode>,
665 pub uncapped: Option<bool>,
669}
670
671impl TimestampingConfig {
672 pub fn new() -> Self {
674 Self::default()
675 }
676
677 pub fn with_mode(self, mode: TimestampingMode) -> Self {
679 Self {
680 mode: Some(mode),
681 ..self
682 }
683 }
684
685 pub fn with_uncapped(self, uncapped: bool) -> Self {
687 Self {
688 uncapped: Some(uncapped),
689 ..self
690 }
691 }
692}
693
694impl From<api::config::TimestampingConfig> for TimestampingConfig {
695 fn from(value: api::config::TimestampingConfig) -> Self {
696 Self {
697 mode: value.mode.map(Into::into),
698 uncapped: value.uncapped,
699 }
700 }
701}
702
703impl From<TimestampingConfig> for api::config::TimestampingConfig {
704 fn from(value: TimestampingConfig) -> Self {
705 Self {
706 mode: value.mode.map(Into::into),
707 uncapped: value.uncapped,
708 }
709 }
710}
711
712#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
713#[non_exhaustive]
714pub struct DeleteOnEmptyConfig {
716 pub min_age_secs: u64,
720}
721
722impl DeleteOnEmptyConfig {
723 pub fn new() -> Self {
725 Self::default()
726 }
727
728 pub fn with_min_age(self, min_age: Duration) -> Self {
730 Self {
731 min_age_secs: min_age.as_secs(),
732 }
733 }
734}
735
736impl From<api::config::DeleteOnEmptyConfig> for DeleteOnEmptyConfig {
737 fn from(value: api::config::DeleteOnEmptyConfig) -> Self {
738 Self {
739 min_age_secs: value.min_age_secs,
740 }
741 }
742}
743
744impl From<DeleteOnEmptyConfig> for api::config::DeleteOnEmptyConfig {
745 fn from(value: DeleteOnEmptyConfig) -> Self {
746 Self {
747 min_age_secs: value.min_age_secs,
748 }
749 }
750}
751
752#[derive(Debug, Clone, Default, PartialEq, Eq)]
753#[non_exhaustive]
754pub struct StreamConfig {
756 pub storage_class: Option<StorageClass>,
760 pub retention_policy: Option<RetentionPolicy>,
764 pub timestamping: Option<TimestampingConfig>,
768 pub delete_on_empty: Option<DeleteOnEmptyConfig>,
772}
773
774impl StreamConfig {
775 pub fn new() -> Self {
777 Self::default()
778 }
779
780 pub fn with_storage_class(self, storage_class: StorageClass) -> Self {
782 Self {
783 storage_class: Some(storage_class),
784 ..self
785 }
786 }
787
788 pub fn with_retention_policy(self, retention_policy: RetentionPolicy) -> Self {
790 Self {
791 retention_policy: Some(retention_policy),
792 ..self
793 }
794 }
795
796 pub fn with_timestamping(self, timestamping: TimestampingConfig) -> Self {
798 Self {
799 timestamping: Some(timestamping),
800 ..self
801 }
802 }
803
804 pub fn with_delete_on_empty(self, delete_on_empty: DeleteOnEmptyConfig) -> Self {
806 Self {
807 delete_on_empty: Some(delete_on_empty),
808 ..self
809 }
810 }
811}
812
813impl From<api::config::StreamConfig> for StreamConfig {
814 fn from(value: api::config::StreamConfig) -> Self {
815 Self {
816 storage_class: value.storage_class.map(Into::into),
817 retention_policy: value.retention_policy.map(Into::into),
818 timestamping: value.timestamping.map(Into::into),
819 delete_on_empty: value.delete_on_empty.map(Into::into),
820 }
821 }
822}
823
824impl From<StreamConfig> for api::config::StreamConfig {
825 fn from(value: StreamConfig) -> Self {
826 Self {
827 storage_class: value.storage_class.map(Into::into),
828 retention_policy: value.retention_policy.map(Into::into),
829 timestamping: value.timestamping.map(Into::into),
830 delete_on_empty: value.delete_on_empty.map(Into::into),
831 }
832 }
833}
834
835#[derive(Debug, Clone, Default, PartialEq, Eq)]
836#[non_exhaustive]
837pub struct BasinConfig {
839 pub default_stream_config: Option<StreamConfig>,
843 pub stream_cipher: Option<EncryptionAlgorithm>,
845 pub create_stream_on_append: bool,
849 pub create_stream_on_read: bool,
853}
854
855impl BasinConfig {
856 pub fn new() -> Self {
858 Self::default()
859 }
860
861 pub fn with_default_stream_config(self, config: StreamConfig) -> Self {
863 Self {
864 default_stream_config: Some(config),
865 ..self
866 }
867 }
868
869 pub fn with_stream_cipher(self, stream_cipher: EncryptionAlgorithm) -> Self {
871 Self {
872 stream_cipher: Some(stream_cipher),
873 ..self
874 }
875 }
876
877 pub fn with_create_stream_on_append(self, create_stream_on_append: bool) -> Self {
880 Self {
881 create_stream_on_append,
882 ..self
883 }
884 }
885
886 pub fn with_create_stream_on_read(self, create_stream_on_read: bool) -> Self {
888 Self {
889 create_stream_on_read,
890 ..self
891 }
892 }
893}
894
895impl From<api::config::BasinConfig> for BasinConfig {
896 fn from(value: api::config::BasinConfig) -> Self {
897 Self {
898 default_stream_config: value.default_stream_config.map(Into::into),
899 stream_cipher: value.stream_cipher.map(Into::into),
900 create_stream_on_append: value.create_stream_on_append,
901 create_stream_on_read: value.create_stream_on_read,
902 }
903 }
904}
905
906impl From<BasinConfig> for api::config::BasinConfig {
907 fn from(value: BasinConfig) -> Self {
908 Self {
909 default_stream_config: value.default_stream_config.map(Into::into),
910 stream_cipher: value.stream_cipher.map(Into::into),
911 create_stream_on_append: value.create_stream_on_append,
912 create_stream_on_read: value.create_stream_on_read,
913 }
914 }
915}
916
917#[derive(Debug, Clone)]
918#[non_exhaustive]
919pub struct CreateBasinInput {
921 pub name: BasinName,
923 pub config: Option<BasinConfig>,
927 pub location: Option<LocationName>,
931 idempotency_token: String,
932}
933
934impl CreateBasinInput {
935 pub fn new(name: BasinName) -> Self {
937 Self {
938 name,
939 config: None,
940 location: None,
941 idempotency_token: idempotency_token(),
942 }
943 }
944
945 pub fn with_config(self, config: BasinConfig) -> Self {
947 Self {
948 config: Some(config),
949 ..self
950 }
951 }
952
953 pub fn with_location<S>(self, location: S) -> Result<Self, ValidationError>
955 where
956 S: TryInto<LocationName>,
957 S::Error: fmt::Display,
958 {
959 let location = location
960 .try_into()
961 .map_err(|e| ValidationError(e.to_string()))?;
962 Ok(Self {
963 location: Some(location),
964 ..self
965 })
966 }
967}
968
969impl From<CreateBasinInput> for (api::basin::CreateBasinRequest, String) {
970 fn from(value: CreateBasinInput) -> Self {
971 (
972 api::basin::CreateBasinRequest {
973 basin: value.name,
974 config: value.config.map(Into::into),
975 location: value.location,
976 },
977 value.idempotency_token,
978 )
979 }
980}
981
982#[derive(Debug, Clone)]
983#[non_exhaustive]
984pub struct EnsureBasinInput {
986 pub name: BasinName,
988 pub config: Option<BasinConfig>,
992 pub location: Option<LocationName>,
997}
998
999impl EnsureBasinInput {
1000 pub fn new(name: BasinName) -> Self {
1002 Self {
1003 name,
1004 config: None,
1005 location: None,
1006 }
1007 }
1008
1009 pub fn with_config(self, config: BasinConfig) -> Self {
1011 Self {
1012 config: Some(config),
1013 ..self
1014 }
1015 }
1016
1017 pub fn with_location<S>(self, location: S) -> Result<Self, ValidationError>
1019 where
1020 S: TryInto<LocationName>,
1021 S::Error: fmt::Display,
1022 {
1023 let location = location
1024 .try_into()
1025 .map_err(|e| ValidationError(e.to_string()))?;
1026 Ok(Self {
1027 location: Some(location),
1028 ..self
1029 })
1030 }
1031}
1032
1033impl From<EnsureBasinInput> for (BasinName, Option<api::basin::EnsureBasinRequest>) {
1034 fn from(value: EnsureBasinInput) -> Self {
1035 let config = value.config;
1036 let request = if config.is_some() || value.location.is_some() {
1037 Some(api::basin::EnsureBasinRequest {
1038 config: config.map(Into::into),
1039 location: value.location,
1040 })
1041 } else {
1042 None
1043 };
1044 (value.name, request)
1045 }
1046}
1047
1048#[derive(Debug, Clone)]
1049pub enum EnsureOutput<T> {
1052 Created(T),
1054 ConfigUpdated(T),
1056 ConfigUnchanged(T),
1058}
1059
1060impl<T> From<ProvisionResult<T>> for EnsureOutput<T> {
1061 fn from(result: ProvisionResult<T>) -> Self {
1062 match result {
1063 ProvisionResult::Created(info) => EnsureOutput::Created(info),
1064 ProvisionResult::Updated(info) => EnsureOutput::ConfigUpdated(info),
1065 ProvisionResult::Noop(info) => EnsureOutput::ConfigUnchanged(info),
1066 }
1067 }
1068}
1069
1070#[derive(Debug, Clone, Default)]
1071#[non_exhaustive]
1072pub struct ListBasinsInput {
1074 pub prefix: BasinNamePrefix,
1078 pub start_after: BasinNameStartAfter,
1082 pub limit: Option<usize>,
1086}
1087
1088impl ListBasinsInput {
1089 pub fn new() -> Self {
1091 Self::default()
1092 }
1093
1094 pub fn with_prefix(self, prefix: BasinNamePrefix) -> Self {
1096 Self { prefix, ..self }
1097 }
1098
1099 pub fn with_start_after(self, start_after: BasinNameStartAfter) -> Self {
1102 Self {
1103 start_after,
1104 ..self
1105 }
1106 }
1107
1108 pub fn with_limit(self, limit: usize) -> Self {
1110 Self {
1111 limit: Some(limit),
1112 ..self
1113 }
1114 }
1115}
1116
1117impl From<ListBasinsInput> for api::basin::ListBasinsRequest {
1118 fn from(value: ListBasinsInput) -> Self {
1119 Self {
1120 prefix: Some(value.prefix),
1121 start_after: Some(value.start_after),
1122 limit: value.limit,
1123 }
1124 }
1125}
1126
1127#[derive(Debug, Clone, Default)]
1128pub struct ListAllBasinsInput {
1130 pub prefix: BasinNamePrefix,
1134 pub start_after: BasinNameStartAfter,
1138 pub include_deleted: bool,
1142}
1143
1144impl ListAllBasinsInput {
1145 pub fn new() -> Self {
1147 Self::default()
1148 }
1149
1150 pub fn with_prefix(self, prefix: BasinNamePrefix) -> Self {
1152 Self { prefix, ..self }
1153 }
1154
1155 pub fn with_start_after(self, start_after: BasinNameStartAfter) -> Self {
1158 Self {
1159 start_after,
1160 ..self
1161 }
1162 }
1163
1164 pub fn with_include_deleted(self, include_deleted: bool) -> Self {
1166 Self {
1167 include_deleted,
1168 ..self
1169 }
1170 }
1171}
1172
1173#[derive(Debug, Clone, PartialEq, Eq)]
1174#[non_exhaustive]
1175pub struct BasinInfo {
1177 pub name: BasinName,
1179 pub location: Option<LocationName>,
1181 pub created_at: S2DateTime,
1183 pub deleted_at: Option<S2DateTime>,
1185}
1186
1187impl TryFrom<api::basin::BasinInfo> for BasinInfo {
1188 type Error = ValidationError;
1189
1190 fn try_from(value: api::basin::BasinInfo) -> Result<Self, Self::Error> {
1191 Ok(Self {
1192 name: value.name,
1193 location: value.location,
1194 created_at: value.created_at.try_into()?,
1195 deleted_at: value.deleted_at.map(S2DateTime::try_from).transpose()?,
1196 })
1197 }
1198}
1199
1200#[derive(Debug, Clone)]
1201#[non_exhaustive]
1202pub struct DeleteBasinInput {
1204 pub name: BasinName,
1206 pub ignore_not_found: bool,
1208}
1209
1210impl DeleteBasinInput {
1211 pub fn new(name: BasinName) -> Self {
1213 Self {
1214 name,
1215 ignore_not_found: false,
1216 }
1217 }
1218
1219 pub fn with_ignore_not_found(self, ignore_not_found: bool) -> Self {
1221 Self {
1222 ignore_not_found,
1223 ..self
1224 }
1225 }
1226}
1227
1228#[derive(Debug, Clone, Default)]
1229#[non_exhaustive]
1230pub struct TimestampingReconfiguration {
1232 pub mode: Maybe<Option<TimestampingMode>>,
1234 pub uncapped: Maybe<Option<bool>>,
1236}
1237
1238impl TimestampingReconfiguration {
1239 pub fn new() -> Self {
1241 Self::default()
1242 }
1243
1244 pub fn with_mode(self, mode: TimestampingMode) -> Self {
1246 Self {
1247 mode: Maybe::Specified(Some(mode)),
1248 ..self
1249 }
1250 }
1251
1252 pub fn with_uncapped(self, uncapped: bool) -> Self {
1254 Self {
1255 uncapped: Maybe::Specified(Some(uncapped)),
1256 ..self
1257 }
1258 }
1259}
1260
1261impl From<TimestampingReconfiguration> for api::config::TimestampingReconfiguration {
1262 fn from(value: TimestampingReconfiguration) -> Self {
1263 Self {
1264 mode: value.mode.map(|m| m.map(Into::into)),
1265 uncapped: value.uncapped,
1266 }
1267 }
1268}
1269
1270#[derive(Debug, Clone, Default)]
1271#[non_exhaustive]
1272pub struct DeleteOnEmptyReconfiguration {
1274 pub min_age_secs: Maybe<Option<u64>>,
1276}
1277
1278impl DeleteOnEmptyReconfiguration {
1279 pub fn new() -> Self {
1281 Self::default()
1282 }
1283
1284 pub fn with_min_age(self, min_age: Duration) -> Self {
1286 Self {
1287 min_age_secs: Maybe::Specified(Some(min_age.as_secs())),
1288 }
1289 }
1290}
1291
1292impl From<DeleteOnEmptyReconfiguration> for api::config::DeleteOnEmptyReconfiguration {
1293 fn from(value: DeleteOnEmptyReconfiguration) -> Self {
1294 Self {
1295 min_age_secs: value.min_age_secs,
1296 }
1297 }
1298}
1299
1300#[derive(Debug, Clone, Default)]
1301#[non_exhaustive]
1302pub struct StreamReconfiguration {
1304 pub storage_class: Maybe<Option<StorageClass>>,
1306 pub retention_policy: Maybe<Option<RetentionPolicy>>,
1308 pub timestamping: Maybe<Option<TimestampingReconfiguration>>,
1310 pub delete_on_empty: Maybe<Option<DeleteOnEmptyReconfiguration>>,
1312}
1313
1314impl StreamReconfiguration {
1315 pub fn new() -> Self {
1317 Self::default()
1318 }
1319
1320 pub fn with_storage_class(self, storage_class: StorageClass) -> Self {
1322 Self {
1323 storage_class: Maybe::Specified(Some(storage_class)),
1324 ..self
1325 }
1326 }
1327
1328 pub fn with_retention_policy(self, retention_policy: RetentionPolicy) -> Self {
1330 Self {
1331 retention_policy: Maybe::Specified(Some(retention_policy)),
1332 ..self
1333 }
1334 }
1335
1336 pub fn with_timestamping(self, timestamping: TimestampingReconfiguration) -> Self {
1338 Self {
1339 timestamping: Maybe::Specified(Some(timestamping)),
1340 ..self
1341 }
1342 }
1343
1344 pub fn with_delete_on_empty(self, delete_on_empty: DeleteOnEmptyReconfiguration) -> Self {
1346 Self {
1347 delete_on_empty: Maybe::Specified(Some(delete_on_empty)),
1348 ..self
1349 }
1350 }
1351}
1352
1353impl From<StreamReconfiguration> for api::config::StreamReconfiguration {
1354 fn from(value: StreamReconfiguration) -> Self {
1355 Self {
1356 storage_class: value.storage_class.map(|m| m.map(Into::into)),
1357 retention_policy: value.retention_policy.map(|m| m.map(Into::into)),
1358 timestamping: value.timestamping.map(|m| m.map(Into::into)),
1359 delete_on_empty: value.delete_on_empty.map(|m| m.map(Into::into)),
1360 }
1361 }
1362}
1363
1364#[derive(Debug, Clone, Default)]
1365#[non_exhaustive]
1366pub struct BasinReconfiguration {
1368 pub default_stream_config: Maybe<Option<StreamReconfiguration>>,
1370 pub stream_cipher: Maybe<Option<EncryptionAlgorithm>>,
1372 pub create_stream_on_append: Maybe<bool>,
1375 pub create_stream_on_read: Maybe<bool>,
1377}
1378
1379impl BasinReconfiguration {
1380 pub fn new() -> Self {
1382 Self::default()
1383 }
1384
1385 pub fn with_default_stream_config(self, config: StreamReconfiguration) -> Self {
1388 Self {
1389 default_stream_config: Maybe::Specified(Some(config)),
1390 ..self
1391 }
1392 }
1393
1394 pub fn with_stream_cipher(self, stream_cipher: EncryptionAlgorithm) -> Self {
1396 Self {
1397 stream_cipher: Maybe::Specified(Some(stream_cipher)),
1398 ..self
1399 }
1400 }
1401
1402 pub fn with_create_stream_on_append(self, create_stream_on_append: bool) -> Self {
1405 Self {
1406 create_stream_on_append: Maybe::Specified(create_stream_on_append),
1407 ..self
1408 }
1409 }
1410
1411 pub fn with_create_stream_on_read(self, create_stream_on_read: bool) -> Self {
1414 Self {
1415 create_stream_on_read: Maybe::Specified(create_stream_on_read),
1416 ..self
1417 }
1418 }
1419}
1420
1421impl From<BasinReconfiguration> for api::config::BasinReconfiguration {
1422 fn from(value: BasinReconfiguration) -> Self {
1423 Self {
1424 default_stream_config: value.default_stream_config.map(|m| m.map(Into::into)),
1425 stream_cipher: value.stream_cipher.map(|m| m.map(Into::into)),
1426 create_stream_on_append: value.create_stream_on_append,
1427 create_stream_on_read: value.create_stream_on_read,
1428 }
1429 }
1430}
1431
1432#[derive(Debug, Clone)]
1433#[non_exhaustive]
1434pub struct ReconfigureBasinInput {
1436 pub name: BasinName,
1438 pub config: BasinReconfiguration,
1440}
1441
1442impl ReconfigureBasinInput {
1443 pub fn new(name: BasinName, config: BasinReconfiguration) -> Self {
1445 Self { name, config }
1446 }
1447}
1448
1449#[derive(Debug, Clone, Default)]
1450#[non_exhaustive]
1451pub struct ListAccessTokensInput {
1453 pub prefix: AccessTokenIdPrefix,
1457 pub start_after: AccessTokenIdStartAfter,
1461 pub limit: Option<usize>,
1465}
1466
1467impl ListAccessTokensInput {
1468 pub fn new() -> Self {
1470 Self::default()
1471 }
1472
1473 pub fn with_prefix(self, prefix: AccessTokenIdPrefix) -> Self {
1475 Self { prefix, ..self }
1476 }
1477
1478 pub fn with_start_after(self, start_after: AccessTokenIdStartAfter) -> Self {
1481 Self {
1482 start_after,
1483 ..self
1484 }
1485 }
1486
1487 pub fn with_limit(self, limit: usize) -> Self {
1489 Self {
1490 limit: Some(limit),
1491 ..self
1492 }
1493 }
1494}
1495
1496impl From<ListAccessTokensInput> for api::access::ListAccessTokensRequest {
1497 fn from(value: ListAccessTokensInput) -> Self {
1498 Self {
1499 prefix: Some(value.prefix),
1500 start_after: Some(value.start_after),
1501 limit: value.limit,
1502 }
1503 }
1504}
1505
1506#[derive(Debug, Clone, Default)]
1507pub struct ListAllAccessTokensInput {
1509 pub prefix: AccessTokenIdPrefix,
1513 pub start_after: AccessTokenIdStartAfter,
1517}
1518
1519impl ListAllAccessTokensInput {
1520 pub fn new() -> Self {
1522 Self::default()
1523 }
1524
1525 pub fn with_prefix(self, prefix: AccessTokenIdPrefix) -> Self {
1527 Self { prefix, ..self }
1528 }
1529
1530 pub fn with_start_after(self, start_after: AccessTokenIdStartAfter) -> Self {
1533 Self {
1534 start_after,
1535 ..self
1536 }
1537 }
1538}
1539
1540#[derive(Debug, Clone, PartialEq, Eq)]
1541#[non_exhaustive]
1542pub struct LocationInfo {
1544 pub name: LocationName,
1546 pub is_private: bool,
1548}
1549
1550impl From<api::location::LocationInfo> for LocationInfo {
1551 fn from(value: api::location::LocationInfo) -> Self {
1552 Self {
1553 name: value.name,
1554 is_private: value.is_private,
1555 }
1556 }
1557}
1558
1559#[derive(Debug, Clone)]
1560#[non_exhaustive]
1561pub struct AccessTokenInfo {
1563 pub id: AccessTokenId,
1565 pub expires_at: S2DateTime,
1567 pub auto_prefix_streams: bool,
1570 pub scope: AccessTokenScope,
1572}
1573
1574impl TryFrom<api::access::AccessTokenInfo> for AccessTokenInfo {
1575 type Error = ValidationError;
1576
1577 fn try_from(value: api::access::AccessTokenInfo) -> Result<Self, Self::Error> {
1578 let expires_at = value
1579 .expires_at
1580 .map(S2DateTime::try_from)
1581 .transpose()?
1582 .ok_or_else(|| ValidationError::from("missing expires_at"))?;
1583 Ok(Self {
1584 id: value.id,
1585 expires_at,
1586 auto_prefix_streams: value.auto_prefix_streams.unwrap_or(false),
1587 scope: value.scope.into(),
1588 })
1589 }
1590}
1591
1592#[derive(Debug, Clone)]
1593pub enum BasinMatcher {
1597 None,
1599 Exact(BasinName),
1601 Prefix(BasinNamePrefix),
1603}
1604
1605#[derive(Debug, Clone)]
1606pub enum StreamMatcher {
1610 None,
1612 Exact(StreamName),
1614 Prefix(StreamNamePrefix),
1616}
1617
1618#[derive(Debug, Clone)]
1619pub enum AccessTokenMatcher {
1623 None,
1625 Exact(AccessTokenId),
1627 Prefix(AccessTokenIdPrefix),
1629}
1630
1631#[derive(Debug, Clone, Default)]
1632#[non_exhaustive]
1633pub struct ReadWritePermissions {
1635 pub read: bool,
1639 pub write: bool,
1643}
1644
1645impl ReadWritePermissions {
1646 pub fn new() -> Self {
1648 Self::default()
1649 }
1650
1651 pub fn read_only() -> Self {
1653 Self {
1654 read: true,
1655 write: false,
1656 }
1657 }
1658
1659 pub fn write_only() -> Self {
1661 Self {
1662 read: false,
1663 write: true,
1664 }
1665 }
1666
1667 pub fn read_write() -> Self {
1669 Self {
1670 read: true,
1671 write: true,
1672 }
1673 }
1674}
1675
1676impl From<ReadWritePermissions> for api::access::ReadWritePermissions {
1677 fn from(value: ReadWritePermissions) -> Self {
1678 Self {
1679 read: Some(value.read),
1680 write: Some(value.write),
1681 }
1682 }
1683}
1684
1685impl From<api::access::ReadWritePermissions> for ReadWritePermissions {
1686 fn from(value: api::access::ReadWritePermissions) -> Self {
1687 Self {
1688 read: value.read.unwrap_or_default(),
1689 write: value.write.unwrap_or_default(),
1690 }
1691 }
1692}
1693
1694#[derive(Debug, Clone, Default)]
1695#[non_exhaustive]
1696pub struct OperationGroupPermissions {
1700 pub account: Option<ReadWritePermissions>,
1704 pub basin: Option<ReadWritePermissions>,
1708 pub stream: Option<ReadWritePermissions>,
1712}
1713
1714impl OperationGroupPermissions {
1715 pub fn new() -> Self {
1717 Self::default()
1718 }
1719
1720 pub fn read_only_all() -> Self {
1722 Self {
1723 account: Some(ReadWritePermissions::read_only()),
1724 basin: Some(ReadWritePermissions::read_only()),
1725 stream: Some(ReadWritePermissions::read_only()),
1726 }
1727 }
1728
1729 pub fn write_only_all() -> Self {
1731 Self {
1732 account: Some(ReadWritePermissions::write_only()),
1733 basin: Some(ReadWritePermissions::write_only()),
1734 stream: Some(ReadWritePermissions::write_only()),
1735 }
1736 }
1737
1738 pub fn read_write_all() -> Self {
1740 Self {
1741 account: Some(ReadWritePermissions::read_write()),
1742 basin: Some(ReadWritePermissions::read_write()),
1743 stream: Some(ReadWritePermissions::read_write()),
1744 }
1745 }
1746
1747 pub fn with_account(self, account: ReadWritePermissions) -> Self {
1749 Self {
1750 account: Some(account),
1751 ..self
1752 }
1753 }
1754
1755 pub fn with_basin(self, basin: ReadWritePermissions) -> Self {
1757 Self {
1758 basin: Some(basin),
1759 ..self
1760 }
1761 }
1762
1763 pub fn with_stream(self, stream: ReadWritePermissions) -> Self {
1765 Self {
1766 stream: Some(stream),
1767 ..self
1768 }
1769 }
1770}
1771
1772impl From<OperationGroupPermissions> for api::access::PermittedOperationGroups {
1773 fn from(value: OperationGroupPermissions) -> Self {
1774 Self {
1775 account: value.account.map(Into::into),
1776 basin: value.basin.map(Into::into),
1777 stream: value.stream.map(Into::into),
1778 }
1779 }
1780}
1781
1782impl From<api::access::PermittedOperationGroups> for OperationGroupPermissions {
1783 fn from(value: api::access::PermittedOperationGroups) -> Self {
1784 Self {
1785 account: value.account.map(Into::into),
1786 basin: value.basin.map(Into::into),
1787 stream: value.stream.map(Into::into),
1788 }
1789 }
1790}
1791
1792#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
1793pub enum Operation {
1797 ListBasins,
1799 CreateBasin,
1801 GetBasinConfig,
1803 DeleteBasin,
1805 ReconfigureBasin,
1807 ListAccessTokens,
1809 IssueAccessToken,
1811 RevokeAccessToken,
1813 GetAccountMetrics,
1815 GetBasinMetrics,
1817 GetStreamMetrics,
1819 ListStreams,
1821 CreateStream,
1823 GetStreamConfig,
1825 DeleteStream,
1827 ReconfigureStream,
1829 CheckTail,
1831 Append,
1833 Read,
1835 Trim,
1837 Fence,
1839 ListLocations,
1841 GetDefaultLocation,
1843 SetDefaultLocation,
1845}
1846
1847impl From<Operation> for api::access::Operation {
1848 fn from(value: Operation) -> Self {
1849 match value {
1850 Operation::ListBasins => api::access::Operation::ListBasins,
1851 Operation::CreateBasin => api::access::Operation::CreateBasin,
1852 Operation::DeleteBasin => api::access::Operation::DeleteBasin,
1853 Operation::ReconfigureBasin => api::access::Operation::ReconfigureBasin,
1854 Operation::GetBasinConfig => api::access::Operation::GetBasinConfig,
1855 Operation::IssueAccessToken => api::access::Operation::IssueAccessToken,
1856 Operation::RevokeAccessToken => api::access::Operation::RevokeAccessToken,
1857 Operation::ListAccessTokens => api::access::Operation::ListAccessTokens,
1858 Operation::ListStreams => api::access::Operation::ListStreams,
1859 Operation::CreateStream => api::access::Operation::CreateStream,
1860 Operation::DeleteStream => api::access::Operation::DeleteStream,
1861 Operation::GetStreamConfig => api::access::Operation::GetStreamConfig,
1862 Operation::ReconfigureStream => api::access::Operation::ReconfigureStream,
1863 Operation::CheckTail => api::access::Operation::CheckTail,
1864 Operation::Append => api::access::Operation::Append,
1865 Operation::Read => api::access::Operation::Read,
1866 Operation::Trim => api::access::Operation::Trim,
1867 Operation::Fence => api::access::Operation::Fence,
1868 Operation::GetAccountMetrics => api::access::Operation::AccountMetrics,
1869 Operation::GetBasinMetrics => api::access::Operation::BasinMetrics,
1870 Operation::GetStreamMetrics => api::access::Operation::StreamMetrics,
1871 Operation::ListLocations => api::access::Operation::ListLocations,
1872 Operation::GetDefaultLocation => api::access::Operation::GetDefaultLocation,
1873 Operation::SetDefaultLocation => api::access::Operation::SetDefaultLocation,
1874 }
1875 }
1876}
1877
1878impl From<api::access::Operation> for Operation {
1879 fn from(value: api::access::Operation) -> Self {
1880 match value {
1881 api::access::Operation::ListBasins => Operation::ListBasins,
1882 api::access::Operation::CreateBasin => Operation::CreateBasin,
1883 api::access::Operation::DeleteBasin => Operation::DeleteBasin,
1884 api::access::Operation::ReconfigureBasin => Operation::ReconfigureBasin,
1885 api::access::Operation::GetBasinConfig => Operation::GetBasinConfig,
1886 api::access::Operation::IssueAccessToken => Operation::IssueAccessToken,
1887 api::access::Operation::RevokeAccessToken => Operation::RevokeAccessToken,
1888 api::access::Operation::ListAccessTokens => Operation::ListAccessTokens,
1889 api::access::Operation::ListStreams => Operation::ListStreams,
1890 api::access::Operation::CreateStream => Operation::CreateStream,
1891 api::access::Operation::DeleteStream => Operation::DeleteStream,
1892 api::access::Operation::GetStreamConfig => Operation::GetStreamConfig,
1893 api::access::Operation::ReconfigureStream => Operation::ReconfigureStream,
1894 api::access::Operation::CheckTail => Operation::CheckTail,
1895 api::access::Operation::Append => Operation::Append,
1896 api::access::Operation::Read => Operation::Read,
1897 api::access::Operation::Trim => Operation::Trim,
1898 api::access::Operation::Fence => Operation::Fence,
1899 api::access::Operation::AccountMetrics => Operation::GetAccountMetrics,
1900 api::access::Operation::BasinMetrics => Operation::GetBasinMetrics,
1901 api::access::Operation::StreamMetrics => Operation::GetStreamMetrics,
1902 api::access::Operation::ListLocations => Operation::ListLocations,
1903 api::access::Operation::GetDefaultLocation => Operation::GetDefaultLocation,
1904 api::access::Operation::SetDefaultLocation => Operation::SetDefaultLocation,
1905 }
1906 }
1907}
1908
1909#[derive(Debug, Clone)]
1910#[non_exhaustive]
1911pub struct AccessTokenScopeInput {
1919 basins: Option<BasinMatcher>,
1920 streams: Option<StreamMatcher>,
1921 access_tokens: Option<AccessTokenMatcher>,
1922 op_group_perms: Option<OperationGroupPermissions>,
1923 ops: HashSet<Operation>,
1924}
1925
1926impl AccessTokenScopeInput {
1927 pub fn from_ops(ops: impl IntoIterator<Item = Operation>) -> Self {
1929 Self {
1930 basins: None,
1931 streams: None,
1932 access_tokens: None,
1933 op_group_perms: None,
1934 ops: ops.into_iter().collect(),
1935 }
1936 }
1937
1938 pub fn from_op_group_perms(op_group_perms: OperationGroupPermissions) -> Self {
1940 Self {
1941 basins: None,
1942 streams: None,
1943 access_tokens: None,
1944 op_group_perms: Some(op_group_perms),
1945 ops: HashSet::default(),
1946 }
1947 }
1948
1949 pub fn with_ops(self, ops: impl IntoIterator<Item = Operation>) -> Self {
1951 Self {
1952 ops: ops.into_iter().collect(),
1953 ..self
1954 }
1955 }
1956
1957 pub fn with_op_group_perms(self, op_group_perms: OperationGroupPermissions) -> Self {
1959 Self {
1960 op_group_perms: Some(op_group_perms),
1961 ..self
1962 }
1963 }
1964
1965 pub fn with_basins(self, basins: BasinMatcher) -> Self {
1969 Self {
1970 basins: Some(basins),
1971 ..self
1972 }
1973 }
1974
1975 pub fn with_streams(self, streams: StreamMatcher) -> Self {
1979 Self {
1980 streams: Some(streams),
1981 ..self
1982 }
1983 }
1984
1985 pub fn with_access_tokens(self, access_tokens: AccessTokenMatcher) -> Self {
1989 Self {
1990 access_tokens: Some(access_tokens),
1991 ..self
1992 }
1993 }
1994}
1995
1996#[derive(Debug, Clone)]
1997#[non_exhaustive]
1998pub struct AccessTokenScope {
2000 pub basins: Option<BasinMatcher>,
2002 pub streams: Option<StreamMatcher>,
2004 pub access_tokens: Option<AccessTokenMatcher>,
2006 pub op_group_perms: Option<OperationGroupPermissions>,
2008 pub ops: HashSet<Operation>,
2010}
2011
2012impl From<api::access::AccessTokenScope> for AccessTokenScope {
2013 fn from(value: api::access::AccessTokenScope) -> Self {
2014 Self {
2015 basins: value.basins.map(|rs| match rs {
2016 api::access::ResourceSet::Exact(api::access::MaybeEmpty::NonEmpty(e)) => {
2017 BasinMatcher::Exact(e)
2018 }
2019 api::access::ResourceSet::Exact(api::access::MaybeEmpty::Empty) => {
2020 BasinMatcher::None
2021 }
2022 api::access::ResourceSet::Prefix(p) => BasinMatcher::Prefix(p),
2023 }),
2024 streams: value.streams.map(|rs| match rs {
2025 api::access::ResourceSet::Exact(api::access::MaybeEmpty::NonEmpty(e)) => {
2026 StreamMatcher::Exact(e)
2027 }
2028 api::access::ResourceSet::Exact(api::access::MaybeEmpty::Empty) => {
2029 StreamMatcher::None
2030 }
2031 api::access::ResourceSet::Prefix(p) => StreamMatcher::Prefix(p),
2032 }),
2033 access_tokens: value.access_tokens.map(|rs| match rs {
2034 api::access::ResourceSet::Exact(api::access::MaybeEmpty::NonEmpty(e)) => {
2035 AccessTokenMatcher::Exact(e)
2036 }
2037 api::access::ResourceSet::Exact(api::access::MaybeEmpty::Empty) => {
2038 AccessTokenMatcher::None
2039 }
2040 api::access::ResourceSet::Prefix(p) => AccessTokenMatcher::Prefix(p),
2041 }),
2042 op_group_perms: value.op_groups.map(Into::into),
2043 ops: value
2044 .ops
2045 .map(|ops| ops.into_iter().map(Into::into).collect())
2046 .unwrap_or_default(),
2047 }
2048 }
2049}
2050
2051impl From<AccessTokenScopeInput> for api::access::AccessTokenScope {
2052 fn from(value: AccessTokenScopeInput) -> Self {
2053 Self {
2054 basins: value.basins.map(|rs| match rs {
2055 BasinMatcher::None => {
2056 api::access::ResourceSet::Exact(api::access::MaybeEmpty::Empty)
2057 }
2058 BasinMatcher::Exact(e) => {
2059 api::access::ResourceSet::Exact(api::access::MaybeEmpty::NonEmpty(e))
2060 }
2061 BasinMatcher::Prefix(p) => api::access::ResourceSet::Prefix(p),
2062 }),
2063 streams: value.streams.map(|rs| match rs {
2064 StreamMatcher::None => {
2065 api::access::ResourceSet::Exact(api::access::MaybeEmpty::Empty)
2066 }
2067 StreamMatcher::Exact(e) => {
2068 api::access::ResourceSet::Exact(api::access::MaybeEmpty::NonEmpty(e))
2069 }
2070 StreamMatcher::Prefix(p) => api::access::ResourceSet::Prefix(p),
2071 }),
2072 access_tokens: value.access_tokens.map(|rs| match rs {
2073 AccessTokenMatcher::None => {
2074 api::access::ResourceSet::Exact(api::access::MaybeEmpty::Empty)
2075 }
2076 AccessTokenMatcher::Exact(e) => {
2077 api::access::ResourceSet::Exact(api::access::MaybeEmpty::NonEmpty(e))
2078 }
2079 AccessTokenMatcher::Prefix(p) => api::access::ResourceSet::Prefix(p),
2080 }),
2081 op_groups: value.op_group_perms.map(Into::into),
2082 ops: if value.ops.is_empty() {
2083 None
2084 } else {
2085 Some(value.ops.into_iter().map(Into::into).collect())
2086 },
2087 }
2088 }
2089}
2090
2091#[derive(Debug, Clone)]
2092#[non_exhaustive]
2093pub struct IssueAccessTokenInput {
2095 pub id: AccessTokenId,
2097 pub expires_at: Option<S2DateTime>,
2102 pub auto_prefix_streams: bool,
2110 pub scope: AccessTokenScopeInput,
2112}
2113
2114impl IssueAccessTokenInput {
2115 pub fn new(id: AccessTokenId, scope: AccessTokenScopeInput) -> Self {
2117 Self {
2118 id,
2119 expires_at: None,
2120 auto_prefix_streams: false,
2121 scope,
2122 }
2123 }
2124
2125 pub fn with_expires_at(self, expires_at: S2DateTime) -> Self {
2127 Self {
2128 expires_at: Some(expires_at),
2129 ..self
2130 }
2131 }
2132
2133 pub fn with_auto_prefix_streams(self, auto_prefix_streams: bool) -> Self {
2136 Self {
2137 auto_prefix_streams,
2138 ..self
2139 }
2140 }
2141}
2142
2143impl From<IssueAccessTokenInput> for api::access::AccessTokenInfo {
2144 fn from(value: IssueAccessTokenInput) -> Self {
2145 Self {
2146 id: value.id,
2147 expires_at: value.expires_at.map(Into::into),
2148 auto_prefix_streams: value.auto_prefix_streams.then_some(true),
2149 scope: value.scope.into(),
2150 }
2151 }
2152}
2153
2154#[derive(Debug, Clone, Copy, PartialEq, Eq)]
2155pub enum TimeseriesInterval {
2157 Minute,
2159 Hour,
2161 Day,
2163}
2164
2165impl From<TimeseriesInterval> for api::metrics::TimeseriesInterval {
2166 fn from(value: TimeseriesInterval) -> Self {
2167 match value {
2168 TimeseriesInterval::Minute => api::metrics::TimeseriesInterval::Minute,
2169 TimeseriesInterval::Hour => api::metrics::TimeseriesInterval::Hour,
2170 TimeseriesInterval::Day => api::metrics::TimeseriesInterval::Day,
2171 }
2172 }
2173}
2174
2175impl From<api::metrics::TimeseriesInterval> for TimeseriesInterval {
2176 fn from(value: api::metrics::TimeseriesInterval) -> Self {
2177 match value {
2178 api::metrics::TimeseriesInterval::Minute => TimeseriesInterval::Minute,
2179 api::metrics::TimeseriesInterval::Hour => TimeseriesInterval::Hour,
2180 api::metrics::TimeseriesInterval::Day => TimeseriesInterval::Day,
2181 }
2182 }
2183}
2184
2185#[derive(Debug, Clone, Copy)]
2186#[non_exhaustive]
2187pub struct TimeRange {
2189 pub start: u32,
2191 pub end: u32,
2193}
2194
2195impl TimeRange {
2196 pub fn new(start: u32, end: u32) -> Self {
2198 Self { start, end }
2199 }
2200}
2201
2202#[derive(Debug, Clone, Copy)]
2203#[non_exhaustive]
2204pub struct TimeRangeAndInterval {
2206 pub start: u32,
2208 pub end: u32,
2210 pub interval: Option<TimeseriesInterval>,
2214}
2215
2216impl TimeRangeAndInterval {
2217 pub fn new(start: u32, end: u32) -> Self {
2219 Self {
2220 start,
2221 end,
2222 interval: None,
2223 }
2224 }
2225
2226 pub fn with_interval(self, interval: TimeseriesInterval) -> Self {
2228 Self {
2229 interval: Some(interval),
2230 ..self
2231 }
2232 }
2233}
2234
2235#[derive(Debug, Clone, Copy)]
2236pub enum AccountMetricSet {
2238 ActiveBasins(TimeRange),
2241 AccountOps(TimeRangeAndInterval),
2248}
2249
2250#[derive(Debug, Clone)]
2251#[non_exhaustive]
2252pub struct GetAccountMetricsInput {
2254 pub set: AccountMetricSet,
2256}
2257
2258impl GetAccountMetricsInput {
2259 pub fn new(set: AccountMetricSet) -> Self {
2261 Self { set }
2262 }
2263}
2264
2265impl From<GetAccountMetricsInput> for api::metrics::AccountMetricSetRequest {
2266 fn from(value: GetAccountMetricsInput) -> Self {
2267 let (set, start, end, interval) = match value.set {
2268 AccountMetricSet::ActiveBasins(args) => (
2269 api::metrics::AccountMetricSet::ActiveBasins,
2270 args.start,
2271 args.end,
2272 None,
2273 ),
2274 AccountMetricSet::AccountOps(args) => (
2275 api::metrics::AccountMetricSet::AccountOps,
2276 args.start,
2277 args.end,
2278 args.interval,
2279 ),
2280 };
2281 Self {
2282 set,
2283 start: Some(start),
2284 end: Some(end),
2285 interval: interval.map(Into::into),
2286 }
2287 }
2288}
2289
2290#[derive(Debug, Clone, Copy)]
2291pub enum BasinMetricSet {
2293 Storage(TimeRange),
2296 AppendOps(TimeRangeAndInterval),
2304 ReadOps(TimeRangeAndInterval),
2312 ReadThroughput(TimeRangeAndInterval),
2319 AppendThroughput(TimeRangeAndInterval),
2326 BasinOps(TimeRangeAndInterval),
2333}
2334
2335#[derive(Debug, Clone)]
2336#[non_exhaustive]
2337pub struct GetBasinMetricsInput {
2339 pub name: BasinName,
2341 pub set: BasinMetricSet,
2343}
2344
2345impl GetBasinMetricsInput {
2346 pub fn new(name: BasinName, set: BasinMetricSet) -> Self {
2348 Self { name, set }
2349 }
2350}
2351
2352impl From<GetBasinMetricsInput> for (BasinName, api::metrics::BasinMetricSetRequest) {
2353 fn from(value: GetBasinMetricsInput) -> Self {
2354 let (set, start, end, interval) = match value.set {
2355 BasinMetricSet::Storage(args) => (
2356 api::metrics::BasinMetricSet::Storage,
2357 args.start,
2358 args.end,
2359 None,
2360 ),
2361 BasinMetricSet::AppendOps(args) => (
2362 api::metrics::BasinMetricSet::AppendOps,
2363 args.start,
2364 args.end,
2365 args.interval,
2366 ),
2367 BasinMetricSet::ReadOps(args) => (
2368 api::metrics::BasinMetricSet::ReadOps,
2369 args.start,
2370 args.end,
2371 args.interval,
2372 ),
2373 BasinMetricSet::ReadThroughput(args) => (
2374 api::metrics::BasinMetricSet::ReadThroughput,
2375 args.start,
2376 args.end,
2377 args.interval,
2378 ),
2379 BasinMetricSet::AppendThroughput(args) => (
2380 api::metrics::BasinMetricSet::AppendThroughput,
2381 args.start,
2382 args.end,
2383 args.interval,
2384 ),
2385 BasinMetricSet::BasinOps(args) => (
2386 api::metrics::BasinMetricSet::BasinOps,
2387 args.start,
2388 args.end,
2389 args.interval,
2390 ),
2391 };
2392 (
2393 value.name,
2394 api::metrics::BasinMetricSetRequest {
2395 set,
2396 start: Some(start),
2397 end: Some(end),
2398 interval: interval.map(Into::into),
2399 },
2400 )
2401 }
2402}
2403
2404#[derive(Debug, Clone, Copy)]
2405pub enum StreamMetricSet {
2407 Storage(TimeRange),
2410}
2411
2412#[derive(Debug, Clone)]
2413#[non_exhaustive]
2414pub struct GetStreamMetricsInput {
2416 pub basin_name: BasinName,
2418 pub stream_name: StreamName,
2420 pub set: StreamMetricSet,
2422}
2423
2424impl GetStreamMetricsInput {
2425 pub fn new(basin_name: BasinName, stream_name: StreamName, set: StreamMetricSet) -> Self {
2428 Self {
2429 basin_name,
2430 stream_name,
2431 set,
2432 }
2433 }
2434}
2435
2436impl From<GetStreamMetricsInput> for (BasinName, StreamName, api::metrics::StreamMetricSetRequest) {
2437 fn from(value: GetStreamMetricsInput) -> Self {
2438 let (set, start, end, interval) = match value.set {
2439 StreamMetricSet::Storage(args) => (
2440 api::metrics::StreamMetricSet::Storage,
2441 args.start,
2442 args.end,
2443 None,
2444 ),
2445 };
2446 (
2447 value.basin_name,
2448 value.stream_name,
2449 api::metrics::StreamMetricSetRequest {
2450 set,
2451 start: Some(start),
2452 end: Some(end),
2453 interval,
2454 },
2455 )
2456 }
2457}
2458
2459#[derive(Debug, Clone, Copy, PartialEq, Eq)]
2460pub enum MetricUnit {
2462 Bytes,
2464 Operations,
2466}
2467
2468impl From<api::metrics::MetricUnit> for MetricUnit {
2469 fn from(value: api::metrics::MetricUnit) -> Self {
2470 match value {
2471 api::metrics::MetricUnit::Bytes => MetricUnit::Bytes,
2472 api::metrics::MetricUnit::Operations => MetricUnit::Operations,
2473 }
2474 }
2475}
2476
2477#[derive(Debug, Clone)]
2478#[non_exhaustive]
2479pub struct ScalarMetric {
2481 pub name: String,
2483 pub unit: MetricUnit,
2485 pub value: f64,
2487}
2488
2489#[derive(Debug, Clone)]
2490#[non_exhaustive]
2491pub struct AccumulationMetric {
2494 pub name: String,
2496 pub unit: MetricUnit,
2498 pub interval: TimeseriesInterval,
2500 pub values: Vec<(u32, f64)>,
2504}
2505
2506#[derive(Debug, Clone)]
2507#[non_exhaustive]
2508pub struct GaugeMetric {
2510 pub name: String,
2512 pub unit: MetricUnit,
2514 pub values: Vec<(u32, f64)>,
2517}
2518
2519#[derive(Debug, Clone)]
2520#[non_exhaustive]
2521pub struct LabelMetric {
2523 pub name: String,
2525 pub values: Vec<String>,
2527}
2528
2529#[derive(Debug, Clone)]
2530pub enum Metric {
2532 Scalar(ScalarMetric),
2534 Accumulation(AccumulationMetric),
2537 Gauge(GaugeMetric),
2539 Label(LabelMetric),
2541}
2542
2543impl From<api::metrics::Metric> for Metric {
2544 fn from(value: api::metrics::Metric) -> Self {
2545 match value {
2546 api::metrics::Metric::Scalar(sm) => Metric::Scalar(ScalarMetric {
2547 name: sm.name.into(),
2548 unit: sm.unit.into(),
2549 value: sm.value,
2550 }),
2551 api::metrics::Metric::Accumulation(am) => Metric::Accumulation(AccumulationMetric {
2552 name: am.name.into(),
2553 unit: am.unit.into(),
2554 interval: am.interval.into(),
2555 values: am.values,
2556 }),
2557 api::metrics::Metric::Gauge(gm) => Metric::Gauge(GaugeMetric {
2558 name: gm.name.into(),
2559 unit: gm.unit.into(),
2560 values: gm.values,
2561 }),
2562 api::metrics::Metric::Label(lm) => Metric::Label(LabelMetric {
2563 name: lm.name.into(),
2564 values: lm.values,
2565 }),
2566 }
2567 }
2568}
2569
2570#[derive(Debug, Clone, Default)]
2571#[non_exhaustive]
2572pub struct ListStreamsInput {
2574 pub prefix: StreamNamePrefix,
2578 pub start_after: StreamNameStartAfter,
2582 pub limit: Option<usize>,
2586}
2587
2588impl ListStreamsInput {
2589 pub fn new() -> Self {
2591 Self::default()
2592 }
2593
2594 pub fn with_prefix(self, prefix: StreamNamePrefix) -> Self {
2596 Self { prefix, ..self }
2597 }
2598
2599 pub fn with_start_after(self, start_after: StreamNameStartAfter) -> Self {
2602 Self {
2603 start_after,
2604 ..self
2605 }
2606 }
2607
2608 pub fn with_limit(self, limit: usize) -> Self {
2610 Self {
2611 limit: Some(limit),
2612 ..self
2613 }
2614 }
2615}
2616
2617impl From<ListStreamsInput> for api::stream::ListStreamsRequest {
2618 fn from(value: ListStreamsInput) -> Self {
2619 Self {
2620 prefix: Some(value.prefix),
2621 start_after: Some(value.start_after),
2622 limit: value.limit,
2623 }
2624 }
2625}
2626
2627#[derive(Debug, Clone, Default)]
2628pub struct ListAllStreamsInput {
2630 pub prefix: StreamNamePrefix,
2634 pub start_after: StreamNameStartAfter,
2638 pub include_deleted: bool,
2642}
2643
2644impl ListAllStreamsInput {
2645 pub fn new() -> Self {
2647 Self::default()
2648 }
2649
2650 pub fn with_prefix(self, prefix: StreamNamePrefix) -> Self {
2652 Self { prefix, ..self }
2653 }
2654
2655 pub fn with_start_after(self, start_after: StreamNameStartAfter) -> Self {
2658 Self {
2659 start_after,
2660 ..self
2661 }
2662 }
2663
2664 pub fn with_include_deleted(self, include_deleted: bool) -> Self {
2666 Self {
2667 include_deleted,
2668 ..self
2669 }
2670 }
2671}
2672
2673#[derive(Debug, Clone, PartialEq, Eq)]
2674#[non_exhaustive]
2675pub struct StreamInfo {
2677 pub name: StreamName,
2679 pub created_at: S2DateTime,
2681 pub deleted_at: Option<S2DateTime>,
2683 pub cipher: Option<EncryptionAlgorithm>,
2685}
2686
2687impl TryFrom<api::stream::StreamInfo> for StreamInfo {
2688 type Error = ValidationError;
2689
2690 fn try_from(value: api::stream::StreamInfo) -> Result<Self, Self::Error> {
2691 Ok(Self {
2692 name: value.name,
2693 created_at: value.created_at.try_into()?,
2694 deleted_at: value.deleted_at.map(S2DateTime::try_from).transpose()?,
2695 cipher: value.cipher.map(Into::into),
2696 })
2697 }
2698}
2699
2700#[derive(Debug, Clone)]
2701#[non_exhaustive]
2702pub struct CreateStreamInput {
2704 pub name: StreamName,
2706 pub config: Option<StreamConfig>,
2710 idempotency_token: String,
2711}
2712
2713impl CreateStreamInput {
2714 pub fn new(name: StreamName) -> Self {
2716 Self {
2717 name,
2718 config: None,
2719 idempotency_token: idempotency_token(),
2720 }
2721 }
2722
2723 pub fn with_config(self, config: StreamConfig) -> Self {
2725 Self {
2726 config: Some(config),
2727 ..self
2728 }
2729 }
2730}
2731
2732impl From<CreateStreamInput> for (api::stream::CreateStreamRequest, String) {
2733 fn from(value: CreateStreamInput) -> Self {
2734 (
2735 api::stream::CreateStreamRequest {
2736 stream: value.name,
2737 config: value.config.map(Into::into),
2738 },
2739 value.idempotency_token,
2740 )
2741 }
2742}
2743
2744#[derive(Debug, Clone)]
2745#[non_exhaustive]
2746pub struct EnsureStreamInput {
2749 pub name: StreamName,
2751 pub config: Option<StreamConfig>,
2755}
2756
2757impl EnsureStreamInput {
2758 pub fn new(name: StreamName) -> Self {
2760 Self { name, config: None }
2761 }
2762
2763 pub fn with_config(self, config: StreamConfig) -> Self {
2765 Self {
2766 config: Some(config),
2767 ..self
2768 }
2769 }
2770}
2771
2772impl From<EnsureStreamInput> for (StreamName, Option<api::config::StreamConfig>) {
2773 fn from(value: EnsureStreamInput) -> Self {
2774 (value.name, value.config.map(Into::into))
2775 }
2776}
2777
2778#[derive(Debug, Clone)]
2779#[non_exhaustive]
2780pub struct DeleteStreamInput {
2782 pub name: StreamName,
2784 pub ignore_not_found: bool,
2786}
2787
2788impl DeleteStreamInput {
2789 pub fn new(name: StreamName) -> Self {
2791 Self {
2792 name,
2793 ignore_not_found: false,
2794 }
2795 }
2796
2797 pub fn with_ignore_not_found(self, ignore_not_found: bool) -> Self {
2799 Self {
2800 ignore_not_found,
2801 ..self
2802 }
2803 }
2804}
2805
2806#[derive(Debug, Clone)]
2807#[non_exhaustive]
2808pub struct ReconfigureStreamInput {
2810 pub name: StreamName,
2812 pub config: StreamReconfiguration,
2814}
2815
2816impl ReconfigureStreamInput {
2817 pub fn new(name: StreamName, config: StreamReconfiguration) -> Self {
2819 Self { name, config }
2820 }
2821}
2822
2823#[derive(Debug, Clone, PartialEq, Eq)]
2824pub struct FencingToken(String);
2830
2831impl FencingToken {
2832 pub fn generate(n: usize) -> Result<Self, ValidationError> {
2834 rand::rng()
2835 .sample_iter(&rand::distr::Alphanumeric)
2836 .take(n)
2837 .map(char::from)
2838 .collect::<String>()
2839 .parse()
2840 }
2841}
2842
2843impl FromStr for FencingToken {
2844 type Err = ValidationError;
2845
2846 fn from_str(s: &str) -> Result<Self, Self::Err> {
2847 if s.len() > MAX_FENCING_TOKEN_LENGTH {
2848 return Err(ValidationError(format!(
2849 "fencing token exceeds {MAX_FENCING_TOKEN_LENGTH} bytes in length",
2850 )));
2851 }
2852 Ok(FencingToken(s.to_string()))
2853 }
2854}
2855
2856impl std::fmt::Display for FencingToken {
2857 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
2858 write!(f, "{}", self.0)
2859 }
2860}
2861
2862impl Deref for FencingToken {
2863 type Target = str;
2864
2865 fn deref(&self) -> &Self::Target {
2866 &self.0
2867 }
2868}
2869
2870#[derive(Debug, Clone, Copy, PartialEq)]
2871#[non_exhaustive]
2872pub struct StreamPosition {
2874 pub seq_num: u64,
2876 pub timestamp: u64,
2879}
2880
2881impl std::fmt::Display for StreamPosition {
2882 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
2883 write!(f, "seq_num={}, timestamp={}", self.seq_num, self.timestamp)
2884 }
2885}
2886
2887impl From<api::stream::proto::StreamPosition> for StreamPosition {
2888 fn from(value: api::stream::proto::StreamPosition) -> Self {
2889 Self {
2890 seq_num: value.seq_num,
2891 timestamp: value.timestamp,
2892 }
2893 }
2894}
2895
2896impl From<api::stream::StreamPosition> for StreamPosition {
2897 fn from(value: api::stream::StreamPosition) -> Self {
2898 Self {
2899 seq_num: value.seq_num,
2900 timestamp: value.timestamp,
2901 }
2902 }
2903}
2904
2905#[derive(Debug, Clone, PartialEq)]
2906#[non_exhaustive]
2907pub struct Header {
2909 pub name: Bytes,
2911 pub value: Bytes,
2913}
2914
2915impl Header {
2916 pub fn new(name: impl Into<Bytes>, value: impl Into<Bytes>) -> Self {
2918 Self {
2919 name: name.into(),
2920 value: value.into(),
2921 }
2922 }
2923}
2924
2925impl From<Header> for api::stream::proto::Header {
2926 fn from(value: Header) -> Self {
2927 Self {
2928 name: value.name,
2929 value: value.value,
2930 }
2931 }
2932}
2933
2934impl From<api::stream::proto::Header> for Header {
2935 fn from(value: api::stream::proto::Header) -> Self {
2936 Self {
2937 name: value.name,
2938 value: value.value,
2939 }
2940 }
2941}
2942
2943#[derive(Debug, Clone, PartialEq)]
2944pub struct AppendRecord {
2946 body: Bytes,
2947 headers: Vec<Header>,
2948 timestamp: Option<u64>,
2949}
2950
2951impl AppendRecord {
2952 fn validate(self) -> Result<Self, ValidationError> {
2953 if self.metered_bytes() > RECORD_BATCH_MAX.bytes {
2954 Err(ValidationError(format!(
2955 "metered_bytes: {} exceeds {}",
2956 self.metered_bytes(),
2957 RECORD_BATCH_MAX.bytes
2958 )))
2959 } else {
2960 Ok(self)
2961 }
2962 }
2963
2964 pub fn new(body: impl Into<Bytes>) -> Result<Self, ValidationError> {
2966 let record = Self {
2967 body: body.into(),
2968 headers: Vec::default(),
2969 timestamp: None,
2970 };
2971 record.validate()
2972 }
2973
2974 pub fn with_headers(
2976 self,
2977 headers: impl IntoIterator<Item = Header>,
2978 ) -> Result<Self, ValidationError> {
2979 let record = Self {
2980 headers: headers.into_iter().collect(),
2981 ..self
2982 };
2983 record.validate()
2984 }
2985
2986 pub fn with_timestamp(self, timestamp: u64) -> Self {
2990 Self {
2991 timestamp: Some(timestamp),
2992 ..self
2993 }
2994 }
2995
2996 pub fn body(&self) -> &[u8] {
2998 &self.body
2999 }
3000
3001 pub fn headers(&self) -> &[Header] {
3003 &self.headers
3004 }
3005
3006 pub fn timestamp(&self) -> Option<u64> {
3008 self.timestamp
3009 }
3010}
3011
3012impl From<AppendRecord> for api::stream::proto::AppendRecord {
3013 fn from(value: AppendRecord) -> Self {
3014 Self {
3015 timestamp: value.timestamp,
3016 headers: value.headers.into_iter().map(Into::into).collect(),
3017 body: value.body,
3018 }
3019 }
3020}
3021
3022pub trait MeteredBytes {
3029 fn metered_bytes(&self) -> usize;
3031}
3032
3033macro_rules! metered_bytes_impl {
3034 ($ty:ty) => {
3035 impl MeteredBytes for $ty {
3036 fn metered_bytes(&self) -> usize {
3037 8 + (2 * self.headers.len())
3038 + self
3039 .headers
3040 .iter()
3041 .map(|h| h.name.len() + h.value.len())
3042 .sum::<usize>()
3043 + self.body.len()
3044 }
3045 }
3046 };
3047}
3048
3049metered_bytes_impl!(AppendRecord);
3050
3051#[derive(Debug, Clone)]
3052pub struct AppendRecordBatch {
3061 records: Vec<AppendRecord>,
3062 metered_bytes: usize,
3063}
3064
3065impl AppendRecordBatch {
3066 pub(crate) fn with_capacity(capacity: usize) -> Self {
3067 Self {
3068 records: Vec::with_capacity(capacity),
3069 metered_bytes: 0,
3070 }
3071 }
3072
3073 pub(crate) fn push(&mut self, record: AppendRecord) {
3074 self.metered_bytes += record.metered_bytes();
3075 self.records.push(record);
3076 }
3077
3078 pub fn try_from_iter<I>(iter: I) -> Result<Self, ValidationError>
3080 where
3081 I: IntoIterator<Item = AppendRecord>,
3082 {
3083 let mut records = Vec::new();
3084 let mut metered_bytes = 0;
3085
3086 for record in iter {
3087 metered_bytes += record.metered_bytes();
3088 records.push(record);
3089
3090 if metered_bytes > RECORD_BATCH_MAX.bytes {
3091 return Err(ValidationError(format!(
3092 "batch size in metered bytes ({metered_bytes}) exceeds {}",
3093 RECORD_BATCH_MAX.bytes
3094 )));
3095 }
3096
3097 if records.len() > RECORD_BATCH_MAX.count {
3098 return Err(ValidationError(format!(
3099 "number of records in the batch exceeds {}",
3100 RECORD_BATCH_MAX.count
3101 )));
3102 }
3103 }
3104
3105 if records.is_empty() {
3106 return Err(ValidationError("batch is empty".into()));
3107 }
3108
3109 Ok(Self {
3110 records,
3111 metered_bytes,
3112 })
3113 }
3114}
3115
3116impl Deref for AppendRecordBatch {
3117 type Target = [AppendRecord];
3118
3119 fn deref(&self) -> &Self::Target {
3120 &self.records
3121 }
3122}
3123
3124impl MeteredBytes for AppendRecordBatch {
3125 fn metered_bytes(&self) -> usize {
3126 self.metered_bytes
3127 }
3128}
3129
3130#[derive(Debug, Clone)]
3131pub enum Command {
3133 Fence {
3135 fencing_token: FencingToken,
3137 },
3138 Trim {
3140 trim_point: u64,
3142 },
3143}
3144
3145#[derive(Debug, Clone)]
3146#[non_exhaustive]
3147pub struct CommandRecord {
3151 pub command: Command,
3153 pub timestamp: Option<u64>,
3155}
3156
3157impl CommandRecord {
3158 const FENCE: &[u8] = b"fence";
3159 const TRIM: &[u8] = b"trim";
3160
3161 pub fn fence(fencing_token: FencingToken) -> Self {
3166 Self {
3167 command: Command::Fence { fencing_token },
3168 timestamp: None,
3169 }
3170 }
3171
3172 pub fn trim(trim_point: u64) -> Self {
3179 Self {
3180 command: Command::Trim { trim_point },
3181 timestamp: None,
3182 }
3183 }
3184
3185 pub fn with_timestamp(self, timestamp: u64) -> Self {
3187 Self {
3188 timestamp: Some(timestamp),
3189 ..self
3190 }
3191 }
3192}
3193
3194impl From<CommandRecord> for AppendRecord {
3195 fn from(value: CommandRecord) -> Self {
3196 let (header_value, body) = match value.command {
3197 Command::Fence { fencing_token } => (
3198 CommandRecord::FENCE,
3199 Bytes::copy_from_slice(fencing_token.as_bytes()),
3200 ),
3201 Command::Trim { trim_point } => (
3202 CommandRecord::TRIM,
3203 Bytes::copy_from_slice(&trim_point.to_be_bytes()),
3204 ),
3205 };
3206 Self {
3207 body,
3208 headers: vec![Header::new("", header_value)],
3209 timestamp: value.timestamp,
3210 }
3211 }
3212}
3213
3214#[derive(Debug, Clone)]
3215#[non_exhaustive]
3216pub struct AppendInput {
3219 pub records: AppendRecordBatch,
3221 pub match_seq_num: Option<u64>,
3225 pub fencing_token: Option<FencingToken>,
3230}
3231
3232impl AppendInput {
3233 pub fn new(records: AppendRecordBatch) -> Self {
3235 Self {
3236 records,
3237 match_seq_num: None,
3238 fencing_token: None,
3239 }
3240 }
3241
3242 pub fn with_match_seq_num(self, match_seq_num: u64) -> Self {
3244 Self {
3245 match_seq_num: Some(match_seq_num),
3246 ..self
3247 }
3248 }
3249
3250 pub fn with_fencing_token(self, fencing_token: FencingToken) -> Self {
3252 Self {
3253 fencing_token: Some(fencing_token),
3254 ..self
3255 }
3256 }
3257}
3258
3259impl From<AppendInput> for api::stream::proto::AppendInput {
3260 fn from(value: AppendInput) -> Self {
3261 Self {
3262 records: value.records.iter().cloned().map(Into::into).collect(),
3263 match_seq_num: value.match_seq_num,
3264 fencing_token: value.fencing_token.map(|t| t.to_string()),
3265 }
3266 }
3267}
3268
3269#[derive(Debug, Clone, PartialEq)]
3270#[non_exhaustive]
3271pub struct AppendAck {
3273 pub start: StreamPosition,
3275 pub end: StreamPosition,
3281 pub tail: StreamPosition,
3286}
3287
3288impl From<api::stream::proto::AppendAck> for AppendAck {
3289 fn from(value: api::stream::proto::AppendAck) -> Self {
3290 Self {
3291 start: value.start.unwrap_or_default().into(),
3292 end: value.end.unwrap_or_default().into(),
3293 tail: value.tail.unwrap_or_default().into(),
3294 }
3295 }
3296}
3297
3298#[derive(Debug, Clone, Copy)]
3299pub enum ReadFrom {
3301 SeqNum(u64),
3303 Timestamp(u64),
3305 TailOffset(u64),
3307}
3308
3309impl Default for ReadFrom {
3310 fn default() -> Self {
3311 Self::SeqNum(0)
3312 }
3313}
3314
3315#[derive(Debug, Default, Clone)]
3316#[non_exhaustive]
3317pub struct ReadStart {
3319 pub from: ReadFrom,
3323 pub clamp_to_tail: bool,
3327}
3328
3329impl ReadStart {
3330 pub fn new() -> Self {
3332 Self::default()
3333 }
3334
3335 pub fn with_from(self, from: ReadFrom) -> Self {
3337 Self { from, ..self }
3338 }
3339
3340 pub fn with_clamp_to_tail(self, clamp_to_tail: bool) -> Self {
3342 Self {
3343 clamp_to_tail,
3344 ..self
3345 }
3346 }
3347}
3348
3349impl From<ReadStart> for api::stream::ReadStart {
3350 fn from(value: ReadStart) -> Self {
3351 let (seq_num, timestamp, tail_offset) = match value.from {
3352 ReadFrom::SeqNum(n) => (Some(n), None, None),
3353 ReadFrom::Timestamp(t) => (None, Some(t), None),
3354 ReadFrom::TailOffset(o) => (None, None, Some(o)),
3355 };
3356 Self {
3357 seq_num,
3358 timestamp,
3359 tail_offset,
3360 clamp: if value.clamp_to_tail {
3361 Some(true)
3362 } else {
3363 None
3364 },
3365 }
3366 }
3367}
3368
3369#[derive(Debug, Clone, Default)]
3370#[non_exhaustive]
3371pub struct ReadLimits {
3373 pub count: Option<usize>,
3377 pub bytes: Option<usize>,
3381}
3382
3383impl ReadLimits {
3384 pub fn new() -> Self {
3386 Self::default()
3387 }
3388
3389 pub fn with_count(self, count: usize) -> Self {
3391 Self {
3392 count: Some(count),
3393 ..self
3394 }
3395 }
3396
3397 pub fn with_bytes(self, bytes: usize) -> Self {
3399 Self {
3400 bytes: Some(bytes),
3401 ..self
3402 }
3403 }
3404}
3405
3406#[derive(Debug, Clone, Default)]
3407#[non_exhaustive]
3408pub struct ReadStop {
3410 pub limits: ReadLimits,
3414 pub until: Option<RangeTo<u64>>,
3418 pub wait: Option<u32>,
3428}
3429
3430impl ReadStop {
3431 pub fn new() -> Self {
3433 Self::default()
3434 }
3435
3436 pub fn with_limits(self, limits: ReadLimits) -> Self {
3438 Self { limits, ..self }
3439 }
3440
3441 pub fn with_until(self, until: RangeTo<u64>) -> Self {
3443 Self {
3444 until: Some(until),
3445 ..self
3446 }
3447 }
3448
3449 pub fn with_wait(self, wait: u32) -> Self {
3451 Self {
3452 wait: Some(wait),
3453 ..self
3454 }
3455 }
3456}
3457
3458impl From<ReadStop> for api::stream::ReadEnd {
3459 fn from(value: ReadStop) -> Self {
3460 Self {
3461 count: value.limits.count,
3462 bytes: value.limits.bytes,
3463 until: value.until.map(|r| r.end),
3464 wait: value.wait,
3465 }
3466 }
3467}
3468
3469#[derive(Debug, Clone, Default)]
3470#[non_exhaustive]
3471pub struct ReadInput {
3474 pub start: ReadStart,
3478 pub stop: ReadStop,
3482 pub ignore_command_records: bool,
3486}
3487
3488impl ReadInput {
3489 pub fn new() -> Self {
3491 Self::default()
3492 }
3493
3494 pub fn with_start(self, start: ReadStart) -> Self {
3496 Self { start, ..self }
3497 }
3498
3499 pub fn with_stop(self, stop: ReadStop) -> Self {
3501 Self { stop, ..self }
3502 }
3503
3504 pub fn with_ignore_command_records(self, ignore_command_records: bool) -> Self {
3506 Self {
3507 ignore_command_records,
3508 ..self
3509 }
3510 }
3511}
3512
3513#[derive(Debug, Clone)]
3514#[non_exhaustive]
3515pub struct SequencedRecord {
3517 pub seq_num: u64,
3519 pub body: Bytes,
3521 pub headers: Vec<Header>,
3523 pub timestamp: u64,
3525}
3526
3527impl SequencedRecord {
3528 #[doc(hidden)]
3529 #[cfg(feature = "_hidden")]
3530 pub fn from_parts(
3531 seq_num: u64,
3532 timestamp: u64,
3533 headers: Vec<Header>,
3534 body: impl Into<Bytes>,
3535 ) -> Self {
3536 Self {
3537 seq_num,
3538 timestamp,
3539 body: body.into(),
3540 headers,
3541 }
3542 }
3543
3544 pub fn is_command_record(&self) -> bool {
3546 self.headers.len() == 1 && *self.headers[0].name == *b""
3547 }
3548}
3549
3550impl From<api::stream::proto::SequencedRecord> for SequencedRecord {
3551 fn from(value: api::stream::proto::SequencedRecord) -> Self {
3552 Self {
3553 seq_num: value.seq_num,
3554 body: value.body,
3555 headers: value.headers.into_iter().map(Into::into).collect(),
3556 timestamp: value.timestamp,
3557 }
3558 }
3559}
3560
3561metered_bytes_impl!(SequencedRecord);
3562
3563#[derive(Debug, Clone)]
3564#[non_exhaustive]
3565pub struct ReadBatch {
3568 pub records: Vec<SequencedRecord>,
3575 pub tail: Option<StreamPosition>,
3580}
3581
3582impl ReadBatch {
3583 pub(crate) fn from_api(batch: api::stream::proto::ReadBatch) -> Self {
3584 Self {
3585 records: batch.records.into_iter().map(Into::into).collect(),
3586 tail: batch.tail.map(Into::into),
3587 }
3588 }
3589}
3590
3591pub type Streaming<T> = Pin<Box<dyn Send + futures::Stream<Item = Result<T, S2Error>>>>;
3593
3594#[derive(Debug, Clone, thiserror::Error)]
3595pub enum AppendConditionFailed {
3597 #[error("fencing token mismatch, expected: {0}")]
3598 FencingTokenMismatch(FencingToken),
3600 #[error("sequence number mismatch, expected: {0}")]
3601 SeqNumMismatch(u64),
3603}
3604
3605impl From<api::stream::AppendConditionFailed> for AppendConditionFailed {
3606 fn from(value: api::stream::AppendConditionFailed) -> Self {
3607 match value {
3608 api::stream::AppendConditionFailed::FencingTokenMismatch(token) => {
3609 AppendConditionFailed::FencingTokenMismatch(FencingToken(token.to_string()))
3610 }
3611 api::stream::AppendConditionFailed::SeqNumMismatch(seq) => {
3612 AppendConditionFailed::SeqNumMismatch(seq)
3613 }
3614 }
3615 }
3616}
3617
3618#[derive(Debug, Clone, thiserror::Error)]
3619pub enum S2Error {
3621 #[error("{0}")]
3622 Client(String),
3624 #[error("malformed access token: {0}")]
3625 MalformedAccessToken(String),
3627 #[error(transparent)]
3628 Validation(#[from] ValidationError),
3630 #[error("{0}")]
3631 AppendConditionFailed(AppendConditionFailed),
3633 #[error("read from an unwritten position. current tail: {0}")]
3634 ReadUnwritten(StreamPosition),
3636 #[error("{0}")]
3637 Server(ErrorResponse),
3639}
3640
3641impl From<ApiError> for S2Error {
3642 fn from(err: ApiError) -> Self {
3643 match err {
3644 ApiError::ReadUnwritten(tail_response) => {
3645 Self::ReadUnwritten(tail_response.tail.into())
3646 }
3647 ApiError::AppendConditionFailed(condition_failed) => {
3648 Self::AppendConditionFailed(condition_failed.into())
3649 }
3650 ApiError::Server(_, response) => Self::Server(response.into()),
3651 ApiError::MalformedAccessToken(err) => Self::MalformedAccessToken(err),
3652 other => Self::Client(other.to_string()),
3653 }
3654 }
3655}
3656
3657#[derive(Debug, Clone, thiserror::Error)]
3658#[error("{code}: {message}")]
3659#[non_exhaustive]
3660pub struct ErrorResponse {
3662 pub code: String,
3664 pub message: String,
3666}
3667
3668impl From<ApiErrorResponse> for ErrorResponse {
3669 fn from(response: ApiErrorResponse) -> Self {
3670 Self {
3671 code: response.code,
3672 message: response.message,
3673 }
3674 }
3675}
3676
3677fn idempotency_token() -> String {
3678 uuid::Uuid::new_v4().simple().to_string()
3679}
3680
3681#[cfg(test)]
3682mod tests {
3683 use proptest::prelude::*;
3684 use rstest::rstest;
3685
3686 use super::*;
3687
3688 type HeaderParts = (Vec<u8>, Vec<u8>);
3689 type AppendRecordParts = (Vec<u8>, Vec<HeaderParts>);
3690
3691 fn byte_vec_strategy(max_len: usize) -> impl Strategy<Value = Vec<u8>> {
3692 prop::collection::vec(any::<u8>(), 0..=max_len)
3693 }
3694
3695 fn header_parts_strategy() -> impl Strategy<Value = HeaderParts> {
3696 (byte_vec_strategy(32), byte_vec_strategy(64))
3697 }
3698
3699 fn string_strategy(max_chars: usize) -> impl Strategy<Value = String> {
3700 prop::collection::vec(any::<char>(), 0..=max_chars)
3701 .prop_map(|chars| chars.into_iter().collect())
3702 }
3703
3704 fn read_from_strategy() -> impl Strategy<Value = ReadFrom> {
3705 prop_oneof![
3706 any::<u64>().prop_map(ReadFrom::SeqNum),
3707 any::<u64>().prop_map(ReadFrom::Timestamp),
3708 any::<u64>().prop_map(ReadFrom::TailOffset),
3709 ]
3710 }
3711
3712 fn append_record_parts_strategy() -> impl Strategy<Value = AppendRecordParts> {
3713 (
3714 byte_vec_strategy(256),
3715 prop::collection::vec(header_parts_strategy(), 0..=16),
3716 )
3717 }
3718
3719 fn proto_stream_position_strategy() -> impl Strategy<Value = api::stream::proto::StreamPosition>
3720 {
3721 (any::<u64>(), any::<u64>()).prop_map(|(seq_num, timestamp)| {
3722 api::stream::proto::StreamPosition { seq_num, timestamp }
3723 })
3724 }
3725
3726 fn headers_from_parts(headers: &[HeaderParts]) -> Vec<Header> {
3727 headers
3728 .iter()
3729 .map(|(name, value)| Header::new(Bytes::from(name.clone()), Bytes::from(value.clone())))
3730 .collect()
3731 }
3732
3733 fn expected_metered_bytes(body: &[u8], headers: &[HeaderParts]) -> usize {
3734 8 + (2 * headers.len())
3735 + headers
3736 .iter()
3737 .map(|(name, value)| name.len() + value.len())
3738 .sum::<usize>()
3739 + body.len()
3740 }
3741
3742 #[test]
3745 fn s2_datetime_parse_valid_rfc3339() {
3746 let dt: S2DateTime = "2024-01-15T12:30:00Z".parse().unwrap();
3747 assert_eq!(dt.to_string(), "2024-01-15T12:30:00Z");
3748 }
3749
3750 #[test]
3751 fn s2_datetime_parse_with_offset() {
3752 let dt: S2DateTime = "2024-06-01T08:00:00+05:30".parse().unwrap();
3753 assert_eq!(dt.to_string(), "2024-06-01T08:00:00+05:30");
3754
3755 let offset_dt: time::OffsetDateTime = dt.into();
3756 assert_eq!(
3757 offset_dt.offset(),
3758 time::UtcOffset::from_hms(5, 30, 0).unwrap()
3759 );
3760 }
3761
3762 #[test]
3763 fn s2_datetime_parse_invalid() {
3764 let err = "not-a-date".parse::<S2DateTime>();
3765 assert!(err.is_err());
3766 }
3767
3768 #[test]
3769 fn s2_datetime_roundtrip_via_offset_datetime() {
3770 let odt = time::OffsetDateTime::from_unix_timestamp(1_700_000_000).unwrap();
3771 let dt = S2DateTime::try_from(odt).unwrap();
3772 let back: time::OffsetDateTime = dt.into();
3773 assert_eq!(odt, back);
3774 }
3775
3776 #[rstest]
3779 #[case::https_with_scheme("https://aws.s2.dev", Scheme::HTTPS)]
3780 #[case::http_with_scheme("http://localhost:8080", Scheme::HTTP)]
3781 #[case::default_https("aws.s2.dev", Scheme::HTTPS)]
3782 fn account_endpoint_parse(#[case] input: &str, #[case] expected_scheme: Scheme) {
3783 let ep: AccountEndpoint = input.parse().unwrap();
3784 assert_eq!(ep.scheme, expected_scheme);
3785 }
3786
3787 #[rstest]
3790 #[case::https_parent_zone("https://{basin}.b.s2.dev", Scheme::HTTPS, true)]
3791 #[case::http_direct("http://localhost:8080", Scheme::HTTP, false)]
3792 #[case::default_https_parent_zone("{basin}.b.s2.dev", Scheme::HTTPS, true)]
3793 fn basin_endpoint_parse(
3794 #[case] input: &str,
3795 #[case] expected_scheme: Scheme,
3796 #[case] expected_parent_zone: bool,
3797 ) {
3798 let ep: BasinEndpoint = input.parse().unwrap();
3799 assert_eq!(ep.scheme, expected_scheme);
3800 assert_eq!(
3801 matches!(ep.authority, BasinAuthority::ParentZone(_)),
3802 expected_parent_zone
3803 );
3804 }
3805
3806 #[test]
3809 fn s2_endpoints_new_requires_same_scheme() {
3810 let account: AccountEndpoint = "https://aws.s2.dev".parse().unwrap();
3811 let basin: BasinEndpoint = "http://localhost:8080".parse().unwrap();
3812 let err = S2Endpoints::new(account, basin);
3813 assert!(err.is_err());
3814 }
3815
3816 #[test]
3817 fn s2_endpoints_new_same_scheme_succeeds() {
3818 let account: AccountEndpoint = "https://aws.s2.dev".parse().unwrap();
3819 let basin: BasinEndpoint = "https://{basin}.b.s2.dev".parse().unwrap();
3820 let ep = S2Endpoints::new(account, basin).unwrap();
3821 assert_eq!(ep.scheme, Scheme::HTTPS);
3822 }
3823
3824 #[rstest]
3827 #[case::none(Compression::None, CompressionAlgorithm::None)]
3828 #[case::gzip(Compression::Gzip, CompressionAlgorithm::Gzip)]
3829 #[case::zstd(Compression::Zstd, CompressionAlgorithm::Zstd)]
3830 fn compression_conversion(#[case] sdk: Compression, #[case] api: CompressionAlgorithm) {
3831 assert_eq!(CompressionAlgorithm::from(sdk), api);
3832 }
3833
3834 #[test]
3837 fn retry_config_defaults() {
3838 let rc = RetryConfig::default();
3839 assert_eq!(rc.max_attempts.get(), 3);
3840 assert_eq!(rc.min_base_delay, Duration::from_millis(100));
3841 assert_eq!(rc.max_base_delay, Duration::from_secs(1));
3842 assert!(matches!(rc.append_retry_policy, AppendRetryPolicy::All));
3843 }
3844
3845 #[test]
3846 fn retry_config_max_retries() {
3847 let rc = RetryConfig::default();
3848 assert_eq!(rc.max_retries(), 2);
3849 }
3850
3851 #[test]
3854 fn s2_config_defaults() {
3855 let cfg = S2Config::new("test-token");
3856 assert_eq!(cfg.connection_timeout, Duration::from_secs(3));
3857 assert_eq!(cfg.request_timeout, Duration::from_secs(5));
3858 assert!(!cfg.insecure_skip_cert_verification);
3859 }
3860
3861 #[rstest]
3864 #[case::standard(StorageClass::Standard)]
3865 #[case::express(StorageClass::Express)]
3866 fn storage_class_roundtrip(#[case] sdk: StorageClass) {
3867 let api: api::config::StorageClass = sdk.into();
3868 let back: StorageClass = api.into();
3869 assert_eq!(back, sdk);
3870 }
3871
3872 #[rstest]
3875 #[case::age(RetentionPolicy::Age(3600))]
3876 #[case::infinite(RetentionPolicy::Infinite)]
3877 fn retention_policy_roundtrip(#[case] sdk: RetentionPolicy) {
3878 let api: api::config::RetentionPolicy = sdk.into();
3879 let back: RetentionPolicy = api.into();
3880 assert_eq!(back, sdk);
3881 }
3882
3883 #[rstest]
3886 #[case::client_prefer(
3887 TimestampingMode::ClientPrefer,
3888 api::config::TimestampingMode::ClientPrefer
3889 )]
3890 #[case::client_require(
3891 TimestampingMode::ClientRequire,
3892 api::config::TimestampingMode::ClientRequire
3893 )]
3894 #[case::arrival(TimestampingMode::Arrival, api::config::TimestampingMode::Arrival)]
3895 fn timestamping_mode_roundtrip(
3896 #[case] sdk: TimestampingMode,
3897 #[case] expected_api: api::config::TimestampingMode,
3898 ) {
3899 let converted: api::config::TimestampingMode = sdk.into();
3900 assert_eq!(converted, expected_api);
3901 let back: TimestampingMode = converted.into();
3902 assert_eq!(back, sdk);
3903 }
3904
3905 #[test]
3908 fn timestamping_config_roundtrip() {
3909 let sdk = TimestampingConfig {
3910 mode: Some(TimestampingMode::Arrival),
3911 uncapped: Some(true),
3912 };
3913 let api: api::config::TimestampingConfig = sdk.into();
3914 let back: TimestampingConfig = api.into();
3915 assert_eq!(back, sdk);
3916 }
3917
3918 #[test]
3921 fn delete_on_empty_config_roundtrip() {
3922 let sdk = DeleteOnEmptyConfig::new().with_min_age(Duration::from_secs(300));
3923 let api: api::config::DeleteOnEmptyConfig = sdk.into();
3924 let back: DeleteOnEmptyConfig = api.into();
3925 assert_eq!(back, sdk);
3926 }
3927
3928 #[test]
3931 fn stream_config_builder_and_roundtrip() {
3932 let sdk = StreamConfig::new()
3933 .with_storage_class(StorageClass::Express)
3934 .with_retention_policy(RetentionPolicy::Age(86400))
3935 .with_timestamping(TimestampingConfig {
3936 mode: Some(TimestampingMode::ClientPrefer),
3937 uncapped: None,
3938 })
3939 .with_delete_on_empty(DeleteOnEmptyConfig { min_age_secs: 60 });
3940 let api: api::config::StreamConfig = sdk.clone().into();
3941 let back: StreamConfig = api.into();
3942 assert_eq!(back, sdk);
3943 }
3944
3945 #[test]
3948 fn basin_config_builder_and_roundtrip() {
3949 let sdk = BasinConfig::new()
3950 .with_default_stream_config(
3951 StreamConfig::new().with_storage_class(StorageClass::Standard),
3952 )
3953 .with_create_stream_on_append(true)
3954 .with_create_stream_on_read(false);
3955 let api: api::config::BasinConfig = sdk.clone().into();
3956 let back: BasinConfig = api.into();
3957 assert_eq!(back, sdk);
3958 }
3959
3960 proptest! {
3963 #[test]
3964 fn fencing_token_parse_accepts_only_within_byte_limit(
3965 token in string_strategy(MAX_FENCING_TOKEN_LENGTH + 8),
3966 ) {
3967 let parsed = token.parse::<FencingToken>();
3968
3969 if token.len() <= MAX_FENCING_TOKEN_LENGTH {
3970 prop_assert_eq!(parsed.unwrap().to_string(), token);
3971 } else {
3972 prop_assert!(parsed.is_err());
3973 }
3974 }
3975 }
3976
3977 #[test]
3980 fn stream_position_display() {
3981 let pos = StreamPosition {
3982 seq_num: 42,
3983 timestamp: 1700000000,
3984 };
3985 assert_eq!(pos.to_string(), "seq_num=42, timestamp=1700000000");
3986 }
3987
3988 proptest! {
3989 #[test]
3990 fn stream_position_conversions_preserve_values(seq_num in any::<u64>(), timestamp in any::<u64>()) {
3991 let proto: StreamPosition = api::stream::proto::StreamPosition {
3992 seq_num,
3993 timestamp,
3994 }
3995 .into();
3996 prop_assert_eq!(proto.seq_num, seq_num);
3997 prop_assert_eq!(proto.timestamp, timestamp);
3998
3999 let api: StreamPosition = api::stream::StreamPosition {
4000 seq_num,
4001 timestamp,
4002 }
4003 .into();
4004 prop_assert_eq!(api.seq_num, seq_num);
4005 prop_assert_eq!(api.timestamp, timestamp);
4006 }
4007 }
4008
4009 proptest! {
4012 #[test]
4013 fn header_proto_roundtrip_preserves_binary_parts(
4014 name in byte_vec_strategy(64),
4015 value in byte_vec_strategy(128),
4016 ) {
4017 let header = Header::new(Bytes::from(name.clone()), Bytes::from(value.clone()));
4018 let proto: api::stream::proto::Header = header.into();
4019 let back: Header = proto.into();
4020
4021 prop_assert_eq!(back.name.as_ref(), name.as_slice());
4022 prop_assert_eq!(back.value.as_ref(), value.as_slice());
4023 }
4024 }
4025
4026 #[test]
4029 fn append_record_too_large() {
4030 let big_body = vec![0u8; RECORD_BATCH_MAX.bytes + 1];
4031 assert!(AppendRecord::new(big_body).is_err());
4032 }
4033
4034 proptest! {
4037 #[test]
4038 fn append_record_preserves_fields_and_metered_byte_formula(
4039 (body, headers) in append_record_parts_strategy(),
4040 timestamp in proptest::option::of(any::<u64>()),
4041 ) {
4042 let mut record = AppendRecord::new(body.clone())
4043 .unwrap()
4044 .with_headers(headers_from_parts(&headers))
4045 .unwrap();
4046 if let Some(timestamp) = timestamp {
4047 record = record.with_timestamp(timestamp);
4048 }
4049
4050 prop_assert_eq!(record.body(), body.as_slice());
4051 prop_assert_eq!(record.headers().len(), headers.len());
4052 prop_assert_eq!(record.timestamp(), timestamp);
4053 prop_assert_eq!(record.metered_bytes(), expected_metered_bytes(&body, &headers));
4054
4055 for (actual, (expected_name, expected_value)) in record.headers().iter().zip(headers.iter()) {
4056 prop_assert_eq!(actual.name.as_ref(), expected_name.as_slice());
4057 prop_assert_eq!(actual.value.as_ref(), expected_value.as_slice());
4058 }
4059 }
4060 }
4061
4062 #[test]
4065 fn append_record_batch_empty_is_err() {
4066 let result = AppendRecordBatch::try_from_iter(vec![]);
4067 assert!(result.is_err());
4068 }
4069
4070 #[test]
4071 fn append_record_batch_too_many_records() {
4072 let records: Vec<_> = (0..1001).map(|_| AppendRecord::new("x").unwrap()).collect();
4073 let result = AppendRecordBatch::try_from_iter(records);
4074 assert!(result.is_err());
4075 }
4076
4077 proptest! {
4078 #[test]
4079 fn append_record_batch_metered_bytes_is_sum_of_records(
4080 records in prop::collection::vec(append_record_parts_strategy(), 1..=32),
4081 ) {
4082 let expected = records
4083 .iter()
4084 .map(|(body, headers)| expected_metered_bytes(body, headers))
4085 .sum::<usize>();
4086 let records = records
4087 .into_iter()
4088 .map(|(body, headers)| {
4089 AppendRecord::new(body)
4090 .unwrap()
4091 .with_headers(headers_from_parts(&headers))
4092 .unwrap()
4093 })
4094 .collect::<Vec<_>>();
4095
4096 let batch = AppendRecordBatch::try_from_iter(records).unwrap();
4097 prop_assert_eq!(batch.metered_bytes(), expected);
4098 prop_assert_eq!(batch.iter().map(MeteredBytes::metered_bytes).sum::<usize>(), expected);
4099 }
4100 }
4101
4102 #[test]
4105 fn command_record_fence() {
4106 let token: FencingToken = "tok".parse().unwrap();
4107 let cmd = CommandRecord::fence(token);
4108 let record: AppendRecord = cmd.into();
4109 assert_eq!(record.headers().len(), 1);
4110 assert_eq!(record.headers()[0].name.as_ref(), b"");
4111 assert_eq!(record.headers()[0].value.as_ref(), b"fence");
4112 assert_eq!(record.body(), b"tok");
4113 }
4114
4115 #[test]
4116 fn command_record_trim() {
4117 let cmd = CommandRecord::trim(42);
4118 let record: AppendRecord = cmd.into();
4119 assert_eq!(record.headers().len(), 1);
4120 assert_eq!(record.headers()[0].value.as_ref(), b"trim");
4121 assert_eq!(record.body(), &42u64.to_be_bytes());
4122 }
4123
4124 #[rstest]
4127 #[case::command(vec![Header::new("", "fence")], true)]
4128 #[case::regular(vec![Header::new("key", "value")], false)]
4129 #[case::no_headers(vec![], false)]
4130 fn sequenced_record_command_detection(#[case] headers: Vec<Header>, #[case] expected: bool) {
4131 let record = SequencedRecord {
4132 seq_num: 0,
4133 body: Bytes::from("data"),
4134 headers,
4135 timestamp: 0,
4136 };
4137 assert_eq!(record.is_command_record(), expected);
4138 }
4139
4140 proptest! {
4143 #[test]
4144 fn read_start_to_api_sets_only_selected_position_field(
4145 from in read_from_strategy(),
4146 clamp_to_tail in any::<bool>(),
4147 ) {
4148 let (seq_num, timestamp, tail_offset) = match from {
4149 ReadFrom::SeqNum(value) => (Some(value), None, None),
4150 ReadFrom::Timestamp(value) => (None, Some(value), None),
4151 ReadFrom::TailOffset(value) => (None, None, Some(value)),
4152 };
4153 let api: api::stream::ReadStart = ReadStart::new()
4154 .with_from(from)
4155 .with_clamp_to_tail(clamp_to_tail)
4156 .into();
4157
4158 prop_assert_eq!(api.seq_num, seq_num);
4159 prop_assert_eq!(api.timestamp, timestamp);
4160 prop_assert_eq!(api.tail_offset, tail_offset);
4161 prop_assert_eq!(api.clamp, clamp_to_tail.then_some(true));
4162 }
4163 }
4164
4165 #[test]
4168 fn read_stop_to_api() {
4169 let stop = ReadStop::new()
4170 .with_limits(ReadLimits::new().with_count(50))
4171 .with_until(..1000)
4172 .with_wait(30);
4173 let api: api::stream::ReadEnd = stop.into();
4174 assert_eq!(api.count, Some(50));
4175 assert_eq!(api.until, Some(1000));
4176 assert_eq!(api.wait, Some(30));
4177 }
4178
4179 #[test]
4182 fn operation_roundtrip_all_variants() {
4183 let variants = [
4184 Operation::ListBasins,
4185 Operation::CreateBasin,
4186 Operation::GetBasinConfig,
4187 Operation::DeleteBasin,
4188 Operation::ReconfigureBasin,
4189 Operation::ListAccessTokens,
4190 Operation::IssueAccessToken,
4191 Operation::RevokeAccessToken,
4192 Operation::GetAccountMetrics,
4193 Operation::GetBasinMetrics,
4194 Operation::GetStreamMetrics,
4195 Operation::ListStreams,
4196 Operation::CreateStream,
4197 Operation::GetStreamConfig,
4198 Operation::DeleteStream,
4199 Operation::ReconfigureStream,
4200 Operation::CheckTail,
4201 Operation::Append,
4202 Operation::Read,
4203 Operation::Trim,
4204 Operation::Fence,
4205 Operation::ListLocations,
4206 Operation::GetDefaultLocation,
4207 Operation::SetDefaultLocation,
4208 ];
4209 for op in variants {
4210 let api_op: api::access::Operation = op.into();
4211 let back: Operation = api_op.into();
4212 assert_eq!(back, op);
4213 }
4214 }
4215
4216 #[test]
4219 fn metric_unit_conversion() {
4220 assert_eq!(
4221 MetricUnit::from(api::metrics::MetricUnit::Bytes),
4222 MetricUnit::Bytes
4223 );
4224 assert_eq!(
4225 MetricUnit::from(api::metrics::MetricUnit::Operations),
4226 MetricUnit::Operations
4227 );
4228 }
4229
4230 proptest! {
4233 #[test]
4234 fn append_ack_from_proto_preserves_present_positions_and_defaults_missing(
4235 start in proptest::option::of(proto_stream_position_strategy()),
4236 end in proptest::option::of(proto_stream_position_strategy()),
4237 tail in proptest::option::of(proto_stream_position_strategy()),
4238 ) {
4239 let expected_start = start.unwrap_or_default();
4240 let expected_end = end.unwrap_or_default();
4241 let expected_tail = tail.unwrap_or_default();
4242 let ack: AppendAck = api::stream::proto::AppendAck { start, end, tail }.into();
4243
4244 prop_assert_eq!(ack.start.seq_num, expected_start.seq_num);
4245 prop_assert_eq!(ack.start.timestamp, expected_start.timestamp);
4246 prop_assert_eq!(ack.end.seq_num, expected_end.seq_num);
4247 prop_assert_eq!(ack.end.timestamp, expected_end.timestamp);
4248 prop_assert_eq!(ack.tail.seq_num, expected_tail.seq_num);
4249 prop_assert_eq!(ack.tail.timestamp, expected_tail.timestamp);
4250 }
4251 }
4252
4253 #[test]
4256 fn read_batch_from_api() {
4257 let proto_batch = api::stream::proto::ReadBatch {
4258 records: vec![api::stream::proto::SequencedRecord {
4259 seq_num: 0,
4260 body: Bytes::from("hi"),
4261 headers: vec![api::stream::proto::Header {
4262 name: Bytes::from("k"),
4263 value: Bytes::from("v"),
4264 }],
4265 timestamp: 42,
4266 }],
4267 tail: Some(api::stream::proto::StreamPosition {
4268 seq_num: 1,
4269 timestamp: 42,
4270 }),
4271 };
4272 let batch = ReadBatch::from_api(proto_batch);
4273 assert_eq!(batch.records.len(), 1);
4274 assert_eq!(batch.records[0].seq_num, 0);
4275 assert_eq!(batch.records[0].timestamp, 42);
4276 assert_eq!(batch.records[0].body.as_ref(), b"hi");
4277 assert_eq!(batch.records[0].headers.len(), 1);
4278 assert_eq!(batch.records[0].headers[0].name.as_ref(), b"k");
4279 assert_eq!(batch.records[0].headers[0].value.as_ref(), b"v");
4280 assert_eq!(
4281 batch.tail,
4282 Some(StreamPosition {
4283 seq_num: 1,
4284 timestamp: 42,
4285 })
4286 );
4287 }
4288
4289 #[test]
4292 fn create_basin_input_to_api() {
4293 let name: BasinName = "test-basin-name".parse().unwrap();
4294 let input = CreateBasinInput::new(name.clone()).with_config(BasinConfig::new());
4295 let (req, token): (api::basin::CreateBasinRequest, String) = input.into();
4296 assert_eq!(req.basin, name);
4297 assert!(req.config.is_some());
4298 assert!(!token.is_empty());
4299 }
4300
4301 #[test]
4304 fn create_stream_input_to_api() {
4305 let name: StreamName = "my-stream".parse().unwrap();
4306 let input = CreateStreamInput::new(name.clone()).with_config(StreamConfig::new());
4307 let (req, token): (api::stream::CreateStreamRequest, String) = input.into();
4308 assert_eq!(req.stream, name);
4309 assert!(req.config.is_some());
4310 assert!(!token.is_empty());
4311 }
4312
4313 #[test]
4316 fn sequenced_record_from_proto() {
4317 let proto = api::stream::proto::SequencedRecord {
4318 seq_num: 99,
4319 body: Bytes::from("data"),
4320 headers: vec![api::stream::proto::Header {
4321 name: Bytes::from("k"),
4322 value: Bytes::from("v"),
4323 }],
4324 timestamp: 1234,
4325 };
4326 let record: SequencedRecord = proto.into();
4327 assert_eq!(record.seq_num, 99);
4328 assert_eq!(record.body.as_ref(), b"data");
4329 assert_eq!(record.headers.len(), 1);
4330 assert_eq!(record.headers[0].name.as_ref(), b"k");
4331 assert_eq!(record.headers[0].value.as_ref(), b"v");
4332 assert_eq!(record.timestamp, 1234);
4333 }
4334
4335 #[test]
4338 fn s2_error_from_api_error_client() {
4339 let url_err = url::Url::parse("not a url").unwrap_err();
4340 let err = ApiError::Url(url_err);
4341 let s2_err: S2Error = err.into();
4342 assert!(matches!(s2_err, S2Error::Client(_)));
4343 }
4344
4345 #[test]
4348 fn error_response_from_api() {
4349 let api_resp = ApiErrorResponse {
4350 code: "not_found".to_string(),
4351 message: "basin not found".to_string(),
4352 };
4353 let resp: ErrorResponse = api_resp.into();
4354 assert_eq!(resp.code, "not_found");
4355 assert_eq!(resp.message, "basin not found");
4356 assert!(resp.to_string().contains("not_found"));
4357 }
4358}