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: "a.s2.dev".try_into().expect("valid authority"),
265 basin_authority: BasinAuthority::ParentZone(
266 "b.s2.dev".try_into().expect("valid authority"),
267 ),
268 }
269 }
270}
271
272#[derive(Debug, Clone, Copy)]
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: Option<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.expires_at.map(S2DateTime::try_from).transpose()?;
1579 Ok(Self {
1580 id: value.id,
1581 expires_at,
1582 auto_prefix_streams: value.auto_prefix_streams,
1583 scope: value.scope.into(),
1584 })
1585 }
1586}
1587
1588#[derive(Debug, Clone)]
1589pub enum BasinMatcher {
1593 None,
1595 Exact(BasinName),
1597 Prefix(BasinNamePrefix),
1599}
1600
1601#[derive(Debug, Clone)]
1602pub enum StreamMatcher {
1606 None,
1608 Exact(StreamName),
1610 Prefix(StreamNamePrefix),
1612}
1613
1614#[derive(Debug, Clone)]
1615pub enum AccessTokenMatcher {
1619 None,
1621 Exact(AccessTokenId),
1623 Prefix(AccessTokenIdPrefix),
1625}
1626
1627#[derive(Debug, Clone, Default)]
1628#[non_exhaustive]
1629pub struct ReadWritePermissions {
1631 pub read: bool,
1635 pub write: bool,
1639}
1640
1641impl ReadWritePermissions {
1642 pub fn new() -> Self {
1644 Self::default()
1645 }
1646
1647 pub fn read_only() -> Self {
1649 Self {
1650 read: true,
1651 write: false,
1652 }
1653 }
1654
1655 pub fn write_only() -> Self {
1657 Self {
1658 read: false,
1659 write: true,
1660 }
1661 }
1662
1663 pub fn read_write() -> Self {
1665 Self {
1666 read: true,
1667 write: true,
1668 }
1669 }
1670}
1671
1672impl From<ReadWritePermissions> for api::access::ReadWritePermissions {
1673 fn from(value: ReadWritePermissions) -> Self {
1674 Self {
1675 read: Some(value.read),
1676 write: Some(value.write),
1677 }
1678 }
1679}
1680
1681impl From<api::access::ReadWritePermissions> for ReadWritePermissions {
1682 fn from(value: api::access::ReadWritePermissions) -> Self {
1683 Self {
1684 read: value.read.unwrap_or_default(),
1685 write: value.write.unwrap_or_default(),
1686 }
1687 }
1688}
1689
1690#[derive(Debug, Clone, Default)]
1691#[non_exhaustive]
1692pub struct OperationGroupPermissions {
1696 pub account: Option<ReadWritePermissions>,
1700 pub basin: Option<ReadWritePermissions>,
1704 pub stream: Option<ReadWritePermissions>,
1708}
1709
1710impl OperationGroupPermissions {
1711 pub fn new() -> Self {
1713 Self::default()
1714 }
1715
1716 pub fn read_only_all() -> Self {
1718 Self {
1719 account: Some(ReadWritePermissions::read_only()),
1720 basin: Some(ReadWritePermissions::read_only()),
1721 stream: Some(ReadWritePermissions::read_only()),
1722 }
1723 }
1724
1725 pub fn write_only_all() -> Self {
1727 Self {
1728 account: Some(ReadWritePermissions::write_only()),
1729 basin: Some(ReadWritePermissions::write_only()),
1730 stream: Some(ReadWritePermissions::write_only()),
1731 }
1732 }
1733
1734 pub fn read_write_all() -> Self {
1736 Self {
1737 account: Some(ReadWritePermissions::read_write()),
1738 basin: Some(ReadWritePermissions::read_write()),
1739 stream: Some(ReadWritePermissions::read_write()),
1740 }
1741 }
1742
1743 pub fn with_account(self, account: ReadWritePermissions) -> Self {
1745 Self {
1746 account: Some(account),
1747 ..self
1748 }
1749 }
1750
1751 pub fn with_basin(self, basin: ReadWritePermissions) -> Self {
1753 Self {
1754 basin: Some(basin),
1755 ..self
1756 }
1757 }
1758
1759 pub fn with_stream(self, stream: ReadWritePermissions) -> Self {
1761 Self {
1762 stream: Some(stream),
1763 ..self
1764 }
1765 }
1766}
1767
1768impl From<OperationGroupPermissions> for api::access::PermittedOperationGroups {
1769 fn from(value: OperationGroupPermissions) -> Self {
1770 Self {
1771 account: value.account.map(Into::into),
1772 basin: value.basin.map(Into::into),
1773 stream: value.stream.map(Into::into),
1774 }
1775 }
1776}
1777
1778impl From<api::access::PermittedOperationGroups> for OperationGroupPermissions {
1779 fn from(value: api::access::PermittedOperationGroups) -> Self {
1780 Self {
1781 account: value.account.map(Into::into),
1782 basin: value.basin.map(Into::into),
1783 stream: value.stream.map(Into::into),
1784 }
1785 }
1786}
1787
1788#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
1789pub enum Operation {
1793 ListBasins,
1795 CreateBasin,
1797 GetBasinConfig,
1799 DeleteBasin,
1801 ReconfigureBasin,
1803 ListAccessTokens,
1805 IssueAccessToken,
1807 RevokeAccessToken,
1809 GetAccountMetrics,
1811 GetBasinMetrics,
1813 GetStreamMetrics,
1815 ListStreams,
1817 CreateStream,
1819 GetStreamConfig,
1821 DeleteStream,
1823 ReconfigureStream,
1825 CheckTail,
1827 Append,
1829 Read,
1831 Trim,
1833 Fence,
1835 ListLocations,
1837 GetDefaultLocation,
1839 SetDefaultLocation,
1841}
1842
1843impl From<Operation> for api::access::Operation {
1844 fn from(value: Operation) -> Self {
1845 match value {
1846 Operation::ListBasins => api::access::Operation::ListBasins,
1847 Operation::CreateBasin => api::access::Operation::CreateBasin,
1848 Operation::DeleteBasin => api::access::Operation::DeleteBasin,
1849 Operation::ReconfigureBasin => api::access::Operation::ReconfigureBasin,
1850 Operation::GetBasinConfig => api::access::Operation::GetBasinConfig,
1851 Operation::IssueAccessToken => api::access::Operation::IssueAccessToken,
1852 Operation::RevokeAccessToken => api::access::Operation::RevokeAccessToken,
1853 Operation::ListAccessTokens => api::access::Operation::ListAccessTokens,
1854 Operation::ListStreams => api::access::Operation::ListStreams,
1855 Operation::CreateStream => api::access::Operation::CreateStream,
1856 Operation::DeleteStream => api::access::Operation::DeleteStream,
1857 Operation::GetStreamConfig => api::access::Operation::GetStreamConfig,
1858 Operation::ReconfigureStream => api::access::Operation::ReconfigureStream,
1859 Operation::CheckTail => api::access::Operation::CheckTail,
1860 Operation::Append => api::access::Operation::Append,
1861 Operation::Read => api::access::Operation::Read,
1862 Operation::Trim => api::access::Operation::Trim,
1863 Operation::Fence => api::access::Operation::Fence,
1864 Operation::GetAccountMetrics => api::access::Operation::AccountMetrics,
1865 Operation::GetBasinMetrics => api::access::Operation::BasinMetrics,
1866 Operation::GetStreamMetrics => api::access::Operation::StreamMetrics,
1867 Operation::ListLocations => api::access::Operation::ListLocations,
1868 Operation::GetDefaultLocation => api::access::Operation::GetDefaultLocation,
1869 Operation::SetDefaultLocation => api::access::Operation::SetDefaultLocation,
1870 }
1871 }
1872}
1873
1874impl From<api::access::Operation> for Operation {
1875 fn from(value: api::access::Operation) -> Self {
1876 match value {
1877 api::access::Operation::ListBasins => Operation::ListBasins,
1878 api::access::Operation::CreateBasin => Operation::CreateBasin,
1879 api::access::Operation::DeleteBasin => Operation::DeleteBasin,
1880 api::access::Operation::ReconfigureBasin => Operation::ReconfigureBasin,
1881 api::access::Operation::GetBasinConfig => Operation::GetBasinConfig,
1882 api::access::Operation::IssueAccessToken => Operation::IssueAccessToken,
1883 api::access::Operation::RevokeAccessToken => Operation::RevokeAccessToken,
1884 api::access::Operation::ListAccessTokens => Operation::ListAccessTokens,
1885 api::access::Operation::ListStreams => Operation::ListStreams,
1886 api::access::Operation::CreateStream => Operation::CreateStream,
1887 api::access::Operation::DeleteStream => Operation::DeleteStream,
1888 api::access::Operation::GetStreamConfig => Operation::GetStreamConfig,
1889 api::access::Operation::ReconfigureStream => Operation::ReconfigureStream,
1890 api::access::Operation::CheckTail => Operation::CheckTail,
1891 api::access::Operation::Append => Operation::Append,
1892 api::access::Operation::Read => Operation::Read,
1893 api::access::Operation::Trim => Operation::Trim,
1894 api::access::Operation::Fence => Operation::Fence,
1895 api::access::Operation::AccountMetrics => Operation::GetAccountMetrics,
1896 api::access::Operation::BasinMetrics => Operation::GetBasinMetrics,
1897 api::access::Operation::StreamMetrics => Operation::GetStreamMetrics,
1898 api::access::Operation::ListLocations => Operation::ListLocations,
1899 api::access::Operation::GetDefaultLocation => Operation::GetDefaultLocation,
1900 api::access::Operation::SetDefaultLocation => Operation::SetDefaultLocation,
1901 }
1902 }
1903}
1904
1905#[derive(Debug, Clone)]
1906#[non_exhaustive]
1907pub struct AccessTokenScopeInput {
1915 basins: Option<BasinMatcher>,
1916 streams: Option<StreamMatcher>,
1917 access_tokens: Option<AccessTokenMatcher>,
1918 op_group_perms: Option<OperationGroupPermissions>,
1919 ops: HashSet<Operation>,
1920}
1921
1922impl AccessTokenScopeInput {
1923 pub fn from_ops(ops: impl IntoIterator<Item = Operation>) -> Self {
1925 Self {
1926 basins: None,
1927 streams: None,
1928 access_tokens: None,
1929 op_group_perms: None,
1930 ops: ops.into_iter().collect(),
1931 }
1932 }
1933
1934 pub fn from_op_group_perms(op_group_perms: OperationGroupPermissions) -> Self {
1936 Self {
1937 basins: None,
1938 streams: None,
1939 access_tokens: None,
1940 op_group_perms: Some(op_group_perms),
1941 ops: HashSet::default(),
1942 }
1943 }
1944
1945 pub fn with_ops(self, ops: impl IntoIterator<Item = Operation>) -> Self {
1947 Self {
1948 ops: ops.into_iter().collect(),
1949 ..self
1950 }
1951 }
1952
1953 pub fn with_op_group_perms(self, op_group_perms: OperationGroupPermissions) -> Self {
1955 Self {
1956 op_group_perms: Some(op_group_perms),
1957 ..self
1958 }
1959 }
1960
1961 pub fn with_basins(self, basins: BasinMatcher) -> Self {
1965 Self {
1966 basins: Some(basins),
1967 ..self
1968 }
1969 }
1970
1971 pub fn with_streams(self, streams: StreamMatcher) -> Self {
1975 Self {
1976 streams: Some(streams),
1977 ..self
1978 }
1979 }
1980
1981 pub fn with_access_tokens(self, access_tokens: AccessTokenMatcher) -> Self {
1985 Self {
1986 access_tokens: Some(access_tokens),
1987 ..self
1988 }
1989 }
1990}
1991
1992#[derive(Debug, Clone)]
1993#[non_exhaustive]
1994pub struct AccessTokenScope {
1996 pub basins: Option<BasinMatcher>,
1998 pub streams: Option<StreamMatcher>,
2000 pub access_tokens: Option<AccessTokenMatcher>,
2002 pub op_group_perms: Option<OperationGroupPermissions>,
2004 pub ops: HashSet<Operation>,
2006}
2007
2008impl From<api::access::AccessTokenScope> for AccessTokenScope {
2009 fn from(value: api::access::AccessTokenScope) -> Self {
2010 Self {
2011 basins: value.basins.map(|rs| match rs {
2012 api::access::ResourceSet::Exact(api::access::MaybeEmpty::NonEmpty(e)) => {
2013 BasinMatcher::Exact(e)
2014 }
2015 api::access::ResourceSet::Exact(api::access::MaybeEmpty::Empty) => {
2016 BasinMatcher::None
2017 }
2018 api::access::ResourceSet::Prefix(p) => BasinMatcher::Prefix(p),
2019 }),
2020 streams: value.streams.map(|rs| match rs {
2021 api::access::ResourceSet::Exact(api::access::MaybeEmpty::NonEmpty(e)) => {
2022 StreamMatcher::Exact(e)
2023 }
2024 api::access::ResourceSet::Exact(api::access::MaybeEmpty::Empty) => {
2025 StreamMatcher::None
2026 }
2027 api::access::ResourceSet::Prefix(p) => StreamMatcher::Prefix(p),
2028 }),
2029 access_tokens: value.access_tokens.map(|rs| match rs {
2030 api::access::ResourceSet::Exact(api::access::MaybeEmpty::NonEmpty(e)) => {
2031 AccessTokenMatcher::Exact(e)
2032 }
2033 api::access::ResourceSet::Exact(api::access::MaybeEmpty::Empty) => {
2034 AccessTokenMatcher::None
2035 }
2036 api::access::ResourceSet::Prefix(p) => AccessTokenMatcher::Prefix(p),
2037 }),
2038 op_group_perms: value.op_groups.map(Into::into),
2039 ops: value
2040 .ops
2041 .map(|ops| ops.into_iter().map(Into::into).collect())
2042 .unwrap_or_default(),
2043 }
2044 }
2045}
2046
2047impl From<AccessTokenScopeInput> for api::access::AccessTokenScope {
2048 fn from(value: AccessTokenScopeInput) -> Self {
2049 Self {
2050 basins: value.basins.map(|rs| match rs {
2051 BasinMatcher::None => {
2052 api::access::ResourceSet::Exact(api::access::MaybeEmpty::Empty)
2053 }
2054 BasinMatcher::Exact(e) => {
2055 api::access::ResourceSet::Exact(api::access::MaybeEmpty::NonEmpty(e))
2056 }
2057 BasinMatcher::Prefix(p) => api::access::ResourceSet::Prefix(p),
2058 }),
2059 streams: value.streams.map(|rs| match rs {
2060 StreamMatcher::None => {
2061 api::access::ResourceSet::Exact(api::access::MaybeEmpty::Empty)
2062 }
2063 StreamMatcher::Exact(e) => {
2064 api::access::ResourceSet::Exact(api::access::MaybeEmpty::NonEmpty(e))
2065 }
2066 StreamMatcher::Prefix(p) => api::access::ResourceSet::Prefix(p),
2067 }),
2068 access_tokens: value.access_tokens.map(|rs| match rs {
2069 AccessTokenMatcher::None => {
2070 api::access::ResourceSet::Exact(api::access::MaybeEmpty::Empty)
2071 }
2072 AccessTokenMatcher::Exact(e) => {
2073 api::access::ResourceSet::Exact(api::access::MaybeEmpty::NonEmpty(e))
2074 }
2075 AccessTokenMatcher::Prefix(p) => api::access::ResourceSet::Prefix(p),
2076 }),
2077 op_groups: value.op_group_perms.map(Into::into),
2078 ops: if value.ops.is_empty() {
2079 None
2080 } else {
2081 Some(value.ops.into_iter().map(Into::into).collect())
2082 },
2083 }
2084 }
2085}
2086
2087#[derive(Debug, Clone)]
2088#[non_exhaustive]
2089pub struct IssueAccessTokenInput {
2091 pub id: AccessTokenId,
2093 pub expires_at: Option<S2DateTime>,
2098 pub auto_prefix_streams: bool,
2106 pub scope: AccessTokenScopeInput,
2108}
2109
2110impl IssueAccessTokenInput {
2111 pub fn new(id: AccessTokenId, scope: AccessTokenScopeInput) -> Self {
2113 Self {
2114 id,
2115 expires_at: None,
2116 auto_prefix_streams: false,
2117 scope,
2118 }
2119 }
2120
2121 pub fn with_expires_at(self, expires_at: S2DateTime) -> Self {
2123 Self {
2124 expires_at: Some(expires_at),
2125 ..self
2126 }
2127 }
2128
2129 pub fn with_auto_prefix_streams(self, auto_prefix_streams: bool) -> Self {
2132 Self {
2133 auto_prefix_streams,
2134 ..self
2135 }
2136 }
2137}
2138
2139impl From<IssueAccessTokenInput> for api::access::IssueAccessTokenRequest {
2140 fn from(value: IssueAccessTokenInput) -> Self {
2141 Self {
2142 id: value.id,
2143 expires_at: value.expires_at.map(Into::into),
2144 auto_prefix_streams: value.auto_prefix_streams.then_some(true),
2145 scope: value.scope.into(),
2146 }
2147 }
2148}
2149
2150#[derive(Debug, Clone, Copy, PartialEq, Eq)]
2151pub enum TimeseriesInterval {
2153 Minute,
2155 Hour,
2157 Day,
2159}
2160
2161impl From<TimeseriesInterval> for api::metrics::TimeseriesInterval {
2162 fn from(value: TimeseriesInterval) -> Self {
2163 match value {
2164 TimeseriesInterval::Minute => api::metrics::TimeseriesInterval::Minute,
2165 TimeseriesInterval::Hour => api::metrics::TimeseriesInterval::Hour,
2166 TimeseriesInterval::Day => api::metrics::TimeseriesInterval::Day,
2167 }
2168 }
2169}
2170
2171impl From<api::metrics::TimeseriesInterval> for TimeseriesInterval {
2172 fn from(value: api::metrics::TimeseriesInterval) -> Self {
2173 match value {
2174 api::metrics::TimeseriesInterval::Minute => TimeseriesInterval::Minute,
2175 api::metrics::TimeseriesInterval::Hour => TimeseriesInterval::Hour,
2176 api::metrics::TimeseriesInterval::Day => TimeseriesInterval::Day,
2177 }
2178 }
2179}
2180
2181#[derive(Debug, Clone, Copy)]
2182#[non_exhaustive]
2183pub struct TimeRange {
2185 pub start: u32,
2187 pub end: u32,
2189}
2190
2191impl TimeRange {
2192 pub fn new(start: u32, end: u32) -> Self {
2194 Self { start, end }
2195 }
2196}
2197
2198#[derive(Debug, Clone, Copy)]
2199#[non_exhaustive]
2200pub struct TimeRangeAndInterval {
2202 pub start: u32,
2204 pub end: u32,
2206 pub interval: Option<TimeseriesInterval>,
2210}
2211
2212impl TimeRangeAndInterval {
2213 pub fn new(start: u32, end: u32) -> Self {
2215 Self {
2216 start,
2217 end,
2218 interval: None,
2219 }
2220 }
2221
2222 pub fn with_interval(self, interval: TimeseriesInterval) -> Self {
2224 Self {
2225 interval: Some(interval),
2226 ..self
2227 }
2228 }
2229}
2230
2231#[derive(Debug, Clone, Copy)]
2232pub enum AccountMetricSet {
2234 ActiveBasins(TimeRange),
2237 AccountOps(TimeRangeAndInterval),
2244}
2245
2246#[derive(Debug, Clone)]
2247#[non_exhaustive]
2248pub struct GetAccountMetricsInput {
2250 pub set: AccountMetricSet,
2252}
2253
2254impl GetAccountMetricsInput {
2255 pub fn new(set: AccountMetricSet) -> Self {
2257 Self { set }
2258 }
2259}
2260
2261impl From<GetAccountMetricsInput> for api::metrics::AccountMetricSetRequest {
2262 fn from(value: GetAccountMetricsInput) -> Self {
2263 let (set, start, end, interval) = match value.set {
2264 AccountMetricSet::ActiveBasins(args) => (
2265 api::metrics::AccountMetricSet::ActiveBasins,
2266 args.start,
2267 args.end,
2268 None,
2269 ),
2270 AccountMetricSet::AccountOps(args) => (
2271 api::metrics::AccountMetricSet::AccountOps,
2272 args.start,
2273 args.end,
2274 args.interval,
2275 ),
2276 };
2277 Self {
2278 set,
2279 start: Some(start),
2280 end: Some(end),
2281 interval: interval.map(Into::into),
2282 }
2283 }
2284}
2285
2286#[derive(Debug, Clone, Copy)]
2287pub enum BasinMetricSet {
2289 Storage(TimeRange),
2292 AppendOps(TimeRangeAndInterval),
2300 ReadOps(TimeRangeAndInterval),
2308 ReadThroughput(TimeRangeAndInterval),
2315 AppendThroughput(TimeRangeAndInterval),
2322 BasinOps(TimeRangeAndInterval),
2329}
2330
2331#[derive(Debug, Clone)]
2332#[non_exhaustive]
2333pub struct GetBasinMetricsInput {
2335 pub name: BasinName,
2337 pub set: BasinMetricSet,
2339}
2340
2341impl GetBasinMetricsInput {
2342 pub fn new(name: BasinName, set: BasinMetricSet) -> Self {
2344 Self { name, set }
2345 }
2346}
2347
2348impl From<GetBasinMetricsInput> for (BasinName, api::metrics::BasinMetricSetRequest) {
2349 fn from(value: GetBasinMetricsInput) -> Self {
2350 let (set, start, end, interval) = match value.set {
2351 BasinMetricSet::Storage(args) => (
2352 api::metrics::BasinMetricSet::Storage,
2353 args.start,
2354 args.end,
2355 None,
2356 ),
2357 BasinMetricSet::AppendOps(args) => (
2358 api::metrics::BasinMetricSet::AppendOps,
2359 args.start,
2360 args.end,
2361 args.interval,
2362 ),
2363 BasinMetricSet::ReadOps(args) => (
2364 api::metrics::BasinMetricSet::ReadOps,
2365 args.start,
2366 args.end,
2367 args.interval,
2368 ),
2369 BasinMetricSet::ReadThroughput(args) => (
2370 api::metrics::BasinMetricSet::ReadThroughput,
2371 args.start,
2372 args.end,
2373 args.interval,
2374 ),
2375 BasinMetricSet::AppendThroughput(args) => (
2376 api::metrics::BasinMetricSet::AppendThroughput,
2377 args.start,
2378 args.end,
2379 args.interval,
2380 ),
2381 BasinMetricSet::BasinOps(args) => (
2382 api::metrics::BasinMetricSet::BasinOps,
2383 args.start,
2384 args.end,
2385 args.interval,
2386 ),
2387 };
2388 (
2389 value.name,
2390 api::metrics::BasinMetricSetRequest {
2391 set,
2392 start: Some(start),
2393 end: Some(end),
2394 interval: interval.map(Into::into),
2395 },
2396 )
2397 }
2398}
2399
2400#[derive(Debug, Clone, Copy)]
2401pub enum StreamMetricSet {
2403 Storage(TimeRange),
2406}
2407
2408#[derive(Debug, Clone)]
2409#[non_exhaustive]
2410pub struct GetStreamMetricsInput {
2412 pub basin_name: BasinName,
2414 pub stream_name: StreamName,
2416 pub set: StreamMetricSet,
2418}
2419
2420impl GetStreamMetricsInput {
2421 pub fn new(basin_name: BasinName, stream_name: StreamName, set: StreamMetricSet) -> Self {
2424 Self {
2425 basin_name,
2426 stream_name,
2427 set,
2428 }
2429 }
2430}
2431
2432impl From<GetStreamMetricsInput> for (BasinName, StreamName, api::metrics::StreamMetricSetRequest) {
2433 fn from(value: GetStreamMetricsInput) -> Self {
2434 let (set, start, end, interval) = match value.set {
2435 StreamMetricSet::Storage(args) => (
2436 api::metrics::StreamMetricSet::Storage,
2437 args.start,
2438 args.end,
2439 None,
2440 ),
2441 };
2442 (
2443 value.basin_name,
2444 value.stream_name,
2445 api::metrics::StreamMetricSetRequest {
2446 set,
2447 start: Some(start),
2448 end: Some(end),
2449 interval,
2450 },
2451 )
2452 }
2453}
2454
2455#[derive(Debug, Clone, Copy, PartialEq, Eq)]
2456pub enum MetricUnit {
2458 Bytes,
2460 Operations,
2462}
2463
2464impl From<api::metrics::MetricUnit> for MetricUnit {
2465 fn from(value: api::metrics::MetricUnit) -> Self {
2466 match value {
2467 api::metrics::MetricUnit::Bytes => MetricUnit::Bytes,
2468 api::metrics::MetricUnit::Operations => MetricUnit::Operations,
2469 }
2470 }
2471}
2472
2473#[derive(Debug, Clone)]
2474#[non_exhaustive]
2475pub struct ScalarMetric {
2477 pub name: String,
2479 pub unit: MetricUnit,
2481 pub value: f64,
2483}
2484
2485#[derive(Debug, Clone)]
2486#[non_exhaustive]
2487pub struct AccumulationMetric {
2490 pub name: String,
2492 pub unit: MetricUnit,
2494 pub interval: TimeseriesInterval,
2496 pub values: Vec<(u32, f64)>,
2500}
2501
2502#[derive(Debug, Clone)]
2503#[non_exhaustive]
2504pub struct GaugeMetric {
2506 pub name: String,
2508 pub unit: MetricUnit,
2510 pub values: Vec<(u32, f64)>,
2513}
2514
2515#[derive(Debug, Clone)]
2516#[non_exhaustive]
2517pub struct LabelMetric {
2519 pub name: String,
2521 pub values: Vec<String>,
2523}
2524
2525#[derive(Debug, Clone)]
2526pub enum Metric {
2528 Scalar(ScalarMetric),
2530 Accumulation(AccumulationMetric),
2533 Gauge(GaugeMetric),
2535 Label(LabelMetric),
2537}
2538
2539impl From<api::metrics::Metric> for Metric {
2540 fn from(value: api::metrics::Metric) -> Self {
2541 match value {
2542 api::metrics::Metric::Scalar(sm) => Metric::Scalar(ScalarMetric {
2543 name: sm.name.into(),
2544 unit: sm.unit.into(),
2545 value: sm.value,
2546 }),
2547 api::metrics::Metric::Accumulation(am) => Metric::Accumulation(AccumulationMetric {
2548 name: am.name.into(),
2549 unit: am.unit.into(),
2550 interval: am.interval.into(),
2551 values: am.values,
2552 }),
2553 api::metrics::Metric::Gauge(gm) => Metric::Gauge(GaugeMetric {
2554 name: gm.name.into(),
2555 unit: gm.unit.into(),
2556 values: gm.values,
2557 }),
2558 api::metrics::Metric::Label(lm) => Metric::Label(LabelMetric {
2559 name: lm.name.into(),
2560 values: lm.values,
2561 }),
2562 }
2563 }
2564}
2565
2566#[derive(Debug, Clone, Default)]
2567#[non_exhaustive]
2568pub struct ListStreamsInput {
2570 pub prefix: StreamNamePrefix,
2574 pub start_after: StreamNameStartAfter,
2578 pub limit: Option<usize>,
2582}
2583
2584impl ListStreamsInput {
2585 pub fn new() -> Self {
2587 Self::default()
2588 }
2589
2590 pub fn with_prefix(self, prefix: StreamNamePrefix) -> Self {
2592 Self { prefix, ..self }
2593 }
2594
2595 pub fn with_start_after(self, start_after: StreamNameStartAfter) -> Self {
2598 Self {
2599 start_after,
2600 ..self
2601 }
2602 }
2603
2604 pub fn with_limit(self, limit: usize) -> Self {
2606 Self {
2607 limit: Some(limit),
2608 ..self
2609 }
2610 }
2611}
2612
2613impl From<ListStreamsInput> for api::stream::ListStreamsRequest {
2614 fn from(value: ListStreamsInput) -> Self {
2615 Self {
2616 prefix: Some(value.prefix),
2617 start_after: Some(value.start_after),
2618 limit: value.limit,
2619 }
2620 }
2621}
2622
2623#[derive(Debug, Clone, Default)]
2624pub struct ListAllStreamsInput {
2626 pub prefix: StreamNamePrefix,
2630 pub start_after: StreamNameStartAfter,
2634 pub include_deleted: bool,
2638}
2639
2640impl ListAllStreamsInput {
2641 pub fn new() -> Self {
2643 Self::default()
2644 }
2645
2646 pub fn with_prefix(self, prefix: StreamNamePrefix) -> Self {
2648 Self { prefix, ..self }
2649 }
2650
2651 pub fn with_start_after(self, start_after: StreamNameStartAfter) -> Self {
2654 Self {
2655 start_after,
2656 ..self
2657 }
2658 }
2659
2660 pub fn with_include_deleted(self, include_deleted: bool) -> Self {
2662 Self {
2663 include_deleted,
2664 ..self
2665 }
2666 }
2667}
2668
2669#[derive(Debug, Clone, PartialEq, Eq)]
2670#[non_exhaustive]
2671pub struct StreamInfo {
2673 pub name: StreamName,
2675 pub created_at: S2DateTime,
2677 pub deleted_at: Option<S2DateTime>,
2679 pub cipher: Option<EncryptionAlgorithm>,
2681}
2682
2683impl TryFrom<api::stream::StreamInfo> for StreamInfo {
2684 type Error = ValidationError;
2685
2686 fn try_from(value: api::stream::StreamInfo) -> Result<Self, Self::Error> {
2687 Ok(Self {
2688 name: value.name,
2689 created_at: value.created_at.try_into()?,
2690 deleted_at: value.deleted_at.map(S2DateTime::try_from).transpose()?,
2691 cipher: value.cipher.map(Into::into),
2692 })
2693 }
2694}
2695
2696#[derive(Debug, Clone)]
2697#[non_exhaustive]
2698pub struct CreateStreamInput {
2700 pub name: StreamName,
2702 pub config: Option<StreamConfig>,
2706 idempotency_token: String,
2707}
2708
2709impl CreateStreamInput {
2710 pub fn new(name: StreamName) -> Self {
2712 Self {
2713 name,
2714 config: None,
2715 idempotency_token: idempotency_token(),
2716 }
2717 }
2718
2719 pub fn with_config(self, config: StreamConfig) -> Self {
2721 Self {
2722 config: Some(config),
2723 ..self
2724 }
2725 }
2726}
2727
2728impl From<CreateStreamInput> for (api::stream::CreateStreamRequest, String) {
2729 fn from(value: CreateStreamInput) -> Self {
2730 (
2731 api::stream::CreateStreamRequest {
2732 stream: value.name,
2733 config: value.config.map(Into::into),
2734 },
2735 value.idempotency_token,
2736 )
2737 }
2738}
2739
2740#[derive(Debug, Clone)]
2741#[non_exhaustive]
2742pub struct EnsureStreamInput {
2745 pub name: StreamName,
2747 pub config: Option<StreamConfig>,
2751}
2752
2753impl EnsureStreamInput {
2754 pub fn new(name: StreamName) -> Self {
2756 Self { name, config: None }
2757 }
2758
2759 pub fn with_config(self, config: StreamConfig) -> Self {
2761 Self {
2762 config: Some(config),
2763 ..self
2764 }
2765 }
2766}
2767
2768impl From<EnsureStreamInput> for (StreamName, Option<api::config::StreamConfig>) {
2769 fn from(value: EnsureStreamInput) -> Self {
2770 (value.name, value.config.map(Into::into))
2771 }
2772}
2773
2774#[derive(Debug, Clone)]
2775#[non_exhaustive]
2776pub struct DeleteStreamInput {
2778 pub name: StreamName,
2780 pub ignore_not_found: bool,
2782}
2783
2784impl DeleteStreamInput {
2785 pub fn new(name: StreamName) -> Self {
2787 Self {
2788 name,
2789 ignore_not_found: false,
2790 }
2791 }
2792
2793 pub fn with_ignore_not_found(self, ignore_not_found: bool) -> Self {
2795 Self {
2796 ignore_not_found,
2797 ..self
2798 }
2799 }
2800}
2801
2802#[derive(Debug, Clone)]
2803#[non_exhaustive]
2804pub struct ReconfigureStreamInput {
2806 pub name: StreamName,
2808 pub config: StreamReconfiguration,
2810}
2811
2812impl ReconfigureStreamInput {
2813 pub fn new(name: StreamName, config: StreamReconfiguration) -> Self {
2815 Self { name, config }
2816 }
2817}
2818
2819#[derive(Debug, Clone, PartialEq, Eq)]
2820pub struct FencingToken(String);
2826
2827impl FencingToken {
2828 pub fn generate(n: usize) -> Result<Self, ValidationError> {
2830 rand::rng()
2831 .sample_iter(&rand::distr::Alphanumeric)
2832 .take(n)
2833 .map(char::from)
2834 .collect::<String>()
2835 .parse()
2836 }
2837}
2838
2839impl FromStr for FencingToken {
2840 type Err = ValidationError;
2841
2842 fn from_str(s: &str) -> Result<Self, Self::Err> {
2843 if s.len() > MAX_FENCING_TOKEN_LENGTH {
2844 return Err(ValidationError(format!(
2845 "fencing token exceeds {MAX_FENCING_TOKEN_LENGTH} bytes in length",
2846 )));
2847 }
2848 Ok(FencingToken(s.to_string()))
2849 }
2850}
2851
2852impl std::fmt::Display for FencingToken {
2853 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
2854 write!(f, "{}", self.0)
2855 }
2856}
2857
2858impl Deref for FencingToken {
2859 type Target = str;
2860
2861 fn deref(&self) -> &Self::Target {
2862 &self.0
2863 }
2864}
2865
2866#[derive(Debug, Clone, Copy, PartialEq)]
2867#[non_exhaustive]
2868pub struct StreamPosition {
2870 pub seq_num: u64,
2872 pub timestamp: u64,
2875}
2876
2877impl std::fmt::Display for StreamPosition {
2878 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
2879 write!(f, "seq_num={}, timestamp={}", self.seq_num, self.timestamp)
2880 }
2881}
2882
2883impl From<api::stream::proto::StreamPosition> for StreamPosition {
2884 fn from(value: api::stream::proto::StreamPosition) -> Self {
2885 Self {
2886 seq_num: value.seq_num,
2887 timestamp: value.timestamp,
2888 }
2889 }
2890}
2891
2892impl From<api::stream::StreamPosition> for StreamPosition {
2893 fn from(value: api::stream::StreamPosition) -> Self {
2894 Self {
2895 seq_num: value.seq_num,
2896 timestamp: value.timestamp,
2897 }
2898 }
2899}
2900
2901#[derive(Debug, Clone, PartialEq)]
2902#[non_exhaustive]
2903pub struct Header {
2905 pub name: Bytes,
2907 pub value: Bytes,
2909}
2910
2911impl Header {
2912 pub fn new(name: impl Into<Bytes>, value: impl Into<Bytes>) -> Self {
2914 Self {
2915 name: name.into(),
2916 value: value.into(),
2917 }
2918 }
2919}
2920
2921impl From<Header> for api::stream::proto::Header {
2922 fn from(value: Header) -> Self {
2923 Self {
2924 name: value.name,
2925 value: value.value,
2926 }
2927 }
2928}
2929
2930impl From<api::stream::proto::Header> for Header {
2931 fn from(value: api::stream::proto::Header) -> Self {
2932 Self {
2933 name: value.name,
2934 value: value.value,
2935 }
2936 }
2937}
2938
2939#[derive(Debug, Clone, PartialEq)]
2940pub struct AppendRecord {
2942 body: Bytes,
2943 headers: Vec<Header>,
2944 timestamp: Option<u64>,
2945}
2946
2947impl AppendRecord {
2948 fn validate(self) -> Result<Self, ValidationError> {
2949 if self.metered_bytes() > RECORD_BATCH_MAX.bytes {
2950 Err(ValidationError(format!(
2951 "metered_bytes: {} exceeds {}",
2952 self.metered_bytes(),
2953 RECORD_BATCH_MAX.bytes
2954 )))
2955 } else {
2956 Ok(self)
2957 }
2958 }
2959
2960 pub fn new(body: impl Into<Bytes>) -> Result<Self, ValidationError> {
2962 let record = Self {
2963 body: body.into(),
2964 headers: Vec::default(),
2965 timestamp: None,
2966 };
2967 record.validate()
2968 }
2969
2970 pub fn with_headers(
2972 self,
2973 headers: impl IntoIterator<Item = Header>,
2974 ) -> Result<Self, ValidationError> {
2975 let record = Self {
2976 headers: headers.into_iter().collect(),
2977 ..self
2978 };
2979 record.validate()
2980 }
2981
2982 pub fn with_timestamp(self, timestamp: u64) -> Self {
2986 Self {
2987 timestamp: Some(timestamp),
2988 ..self
2989 }
2990 }
2991
2992 pub fn body(&self) -> &[u8] {
2994 &self.body
2995 }
2996
2997 pub fn headers(&self) -> &[Header] {
2999 &self.headers
3000 }
3001
3002 pub fn timestamp(&self) -> Option<u64> {
3004 self.timestamp
3005 }
3006}
3007
3008impl From<AppendRecord> for api::stream::proto::AppendRecord {
3009 fn from(value: AppendRecord) -> Self {
3010 Self {
3011 timestamp: value.timestamp,
3012 headers: value.headers.into_iter().map(Into::into).collect(),
3013 body: value.body,
3014 }
3015 }
3016}
3017
3018pub trait MeteredBytes {
3025 fn metered_bytes(&self) -> usize;
3027}
3028
3029macro_rules! metered_bytes_impl {
3030 ($ty:ty) => {
3031 impl MeteredBytes for $ty {
3032 fn metered_bytes(&self) -> usize {
3033 8 + (2 * self.headers.len())
3034 + self
3035 .headers
3036 .iter()
3037 .map(|h| h.name.len() + h.value.len())
3038 .sum::<usize>()
3039 + self.body.len()
3040 }
3041 }
3042 };
3043}
3044
3045metered_bytes_impl!(AppendRecord);
3046
3047#[derive(Debug, Clone)]
3048pub struct AppendRecordBatch {
3057 records: Vec<AppendRecord>,
3058 metered_bytes: usize,
3059}
3060
3061impl AppendRecordBatch {
3062 pub(crate) fn with_capacity(capacity: usize) -> Self {
3063 Self {
3064 records: Vec::with_capacity(capacity),
3065 metered_bytes: 0,
3066 }
3067 }
3068
3069 pub(crate) fn push(&mut self, record: AppendRecord) {
3070 self.metered_bytes += record.metered_bytes();
3071 self.records.push(record);
3072 }
3073
3074 pub fn try_from_iter<I>(iter: I) -> Result<Self, ValidationError>
3076 where
3077 I: IntoIterator<Item = AppendRecord>,
3078 {
3079 let mut records = Vec::new();
3080 let mut metered_bytes = 0;
3081
3082 for record in iter {
3083 metered_bytes += record.metered_bytes();
3084 records.push(record);
3085
3086 if metered_bytes > RECORD_BATCH_MAX.bytes {
3087 return Err(ValidationError(format!(
3088 "batch size in metered bytes ({metered_bytes}) exceeds {}",
3089 RECORD_BATCH_MAX.bytes
3090 )));
3091 }
3092
3093 if records.len() > RECORD_BATCH_MAX.count {
3094 return Err(ValidationError(format!(
3095 "number of records in the batch exceeds {}",
3096 RECORD_BATCH_MAX.count
3097 )));
3098 }
3099 }
3100
3101 if records.is_empty() {
3102 return Err(ValidationError("batch is empty".into()));
3103 }
3104
3105 Ok(Self {
3106 records,
3107 metered_bytes,
3108 })
3109 }
3110}
3111
3112impl Deref for AppendRecordBatch {
3113 type Target = [AppendRecord];
3114
3115 fn deref(&self) -> &Self::Target {
3116 &self.records
3117 }
3118}
3119
3120impl MeteredBytes for AppendRecordBatch {
3121 fn metered_bytes(&self) -> usize {
3122 self.metered_bytes
3123 }
3124}
3125
3126#[derive(Debug, Clone)]
3127pub enum Command {
3129 Fence {
3131 fencing_token: FencingToken,
3133 },
3134 Trim {
3136 trim_point: u64,
3138 },
3139}
3140
3141#[derive(Debug, Clone)]
3142#[non_exhaustive]
3143pub struct CommandRecord {
3147 pub command: Command,
3149 pub timestamp: Option<u64>,
3151}
3152
3153impl CommandRecord {
3154 const FENCE: &[u8] = b"fence";
3155 const TRIM: &[u8] = b"trim";
3156
3157 pub fn fence(fencing_token: FencingToken) -> Self {
3162 Self {
3163 command: Command::Fence { fencing_token },
3164 timestamp: None,
3165 }
3166 }
3167
3168 pub fn trim(trim_point: u64) -> Self {
3175 Self {
3176 command: Command::Trim { trim_point },
3177 timestamp: None,
3178 }
3179 }
3180
3181 pub fn with_timestamp(self, timestamp: u64) -> Self {
3183 Self {
3184 timestamp: Some(timestamp),
3185 ..self
3186 }
3187 }
3188}
3189
3190impl From<CommandRecord> for AppendRecord {
3191 fn from(value: CommandRecord) -> Self {
3192 let (header_value, body) = match value.command {
3193 Command::Fence { fencing_token } => (
3194 CommandRecord::FENCE,
3195 Bytes::copy_from_slice(fencing_token.as_bytes()),
3196 ),
3197 Command::Trim { trim_point } => (
3198 CommandRecord::TRIM,
3199 Bytes::copy_from_slice(&trim_point.to_be_bytes()),
3200 ),
3201 };
3202 Self {
3203 body,
3204 headers: vec![Header::new("", header_value)],
3205 timestamp: value.timestamp,
3206 }
3207 }
3208}
3209
3210#[derive(Debug, Clone)]
3211#[non_exhaustive]
3212pub struct AppendInput {
3215 pub records: AppendRecordBatch,
3217 pub match_seq_num: Option<u64>,
3221 pub fencing_token: Option<FencingToken>,
3226}
3227
3228impl AppendInput {
3229 pub fn new(records: AppendRecordBatch) -> Self {
3231 Self {
3232 records,
3233 match_seq_num: None,
3234 fencing_token: None,
3235 }
3236 }
3237
3238 pub fn with_match_seq_num(self, match_seq_num: u64) -> Self {
3240 Self {
3241 match_seq_num: Some(match_seq_num),
3242 ..self
3243 }
3244 }
3245
3246 pub fn with_fencing_token(self, fencing_token: FencingToken) -> Self {
3248 Self {
3249 fencing_token: Some(fencing_token),
3250 ..self
3251 }
3252 }
3253}
3254
3255impl From<AppendInput> for api::stream::proto::AppendInput {
3256 fn from(value: AppendInput) -> Self {
3257 Self {
3258 records: value.records.iter().cloned().map(Into::into).collect(),
3259 match_seq_num: value.match_seq_num,
3260 fencing_token: value.fencing_token.map(|t| t.to_string()),
3261 }
3262 }
3263}
3264
3265#[derive(Debug, Clone, PartialEq)]
3266#[non_exhaustive]
3267pub struct AppendAck {
3269 pub start: StreamPosition,
3271 pub end: StreamPosition,
3277 pub tail: StreamPosition,
3282}
3283
3284impl From<api::stream::proto::AppendAck> for AppendAck {
3285 fn from(value: api::stream::proto::AppendAck) -> Self {
3286 Self {
3287 start: value.start.unwrap_or_default().into(),
3288 end: value.end.unwrap_or_default().into(),
3289 tail: value.tail.unwrap_or_default().into(),
3290 }
3291 }
3292}
3293
3294#[derive(Debug, Clone, Copy)]
3295pub enum ReadFrom {
3297 SeqNum(u64),
3299 Timestamp(u64),
3301 TailOffset(u64),
3303}
3304
3305impl Default for ReadFrom {
3306 fn default() -> Self {
3307 Self::SeqNum(0)
3308 }
3309}
3310
3311#[derive(Debug, Default, Clone)]
3312#[non_exhaustive]
3313pub struct ReadStart {
3315 pub from: ReadFrom,
3319 pub clamp_to_tail: bool,
3323}
3324
3325impl ReadStart {
3326 pub fn new() -> Self {
3328 Self::default()
3329 }
3330
3331 pub fn with_from(self, from: ReadFrom) -> Self {
3333 Self { from, ..self }
3334 }
3335
3336 pub fn with_clamp_to_tail(self, clamp_to_tail: bool) -> Self {
3338 Self {
3339 clamp_to_tail,
3340 ..self
3341 }
3342 }
3343}
3344
3345impl From<ReadStart> for api::stream::ReadStart {
3346 fn from(value: ReadStart) -> Self {
3347 let (seq_num, timestamp, tail_offset) = match value.from {
3348 ReadFrom::SeqNum(n) => (Some(n), None, None),
3349 ReadFrom::Timestamp(t) => (None, Some(t), None),
3350 ReadFrom::TailOffset(o) => (None, None, Some(o)),
3351 };
3352 Self {
3353 seq_num,
3354 timestamp,
3355 tail_offset,
3356 clamp: if value.clamp_to_tail {
3357 Some(true)
3358 } else {
3359 None
3360 },
3361 }
3362 }
3363}
3364
3365#[derive(Debug, Clone, Default)]
3366#[non_exhaustive]
3367pub struct ReadLimits {
3369 pub count: Option<usize>,
3373 pub bytes: Option<usize>,
3377}
3378
3379impl ReadLimits {
3380 pub fn new() -> Self {
3382 Self::default()
3383 }
3384
3385 pub fn with_count(self, count: usize) -> Self {
3387 Self {
3388 count: Some(count),
3389 ..self
3390 }
3391 }
3392
3393 pub fn with_bytes(self, bytes: usize) -> Self {
3395 Self {
3396 bytes: Some(bytes),
3397 ..self
3398 }
3399 }
3400}
3401
3402#[derive(Debug, Clone, Default)]
3403#[non_exhaustive]
3404pub struct ReadStop {
3406 pub limits: ReadLimits,
3410 pub until: Option<RangeTo<u64>>,
3414 pub wait: Option<u32>,
3424}
3425
3426impl ReadStop {
3427 pub fn new() -> Self {
3429 Self::default()
3430 }
3431
3432 pub fn with_limits(self, limits: ReadLimits) -> Self {
3434 Self { limits, ..self }
3435 }
3436
3437 pub fn with_until(self, until: RangeTo<u64>) -> Self {
3439 Self {
3440 until: Some(until),
3441 ..self
3442 }
3443 }
3444
3445 pub fn with_wait(self, wait: u32) -> Self {
3447 Self {
3448 wait: Some(wait),
3449 ..self
3450 }
3451 }
3452}
3453
3454impl From<ReadStop> for api::stream::ReadEnd {
3455 fn from(value: ReadStop) -> Self {
3456 Self {
3457 count: value.limits.count,
3458 bytes: value.limits.bytes,
3459 until: value.until.map(|r| r.end),
3460 wait: value.wait,
3461 }
3462 }
3463}
3464
3465#[derive(Debug, Clone, Default)]
3466#[non_exhaustive]
3467pub struct ReadInput {
3470 pub start: ReadStart,
3474 pub stop: ReadStop,
3478 pub ignore_command_records: bool,
3482}
3483
3484impl ReadInput {
3485 pub fn new() -> Self {
3487 Self::default()
3488 }
3489
3490 pub fn with_start(self, start: ReadStart) -> Self {
3492 Self { start, ..self }
3493 }
3494
3495 pub fn with_stop(self, stop: ReadStop) -> Self {
3497 Self { stop, ..self }
3498 }
3499
3500 pub fn with_ignore_command_records(self, ignore_command_records: bool) -> Self {
3502 Self {
3503 ignore_command_records,
3504 ..self
3505 }
3506 }
3507}
3508
3509#[derive(Debug, Clone)]
3510#[non_exhaustive]
3511pub struct SequencedRecord {
3513 pub seq_num: u64,
3515 pub body: Bytes,
3517 pub headers: Vec<Header>,
3519 pub timestamp: u64,
3521}
3522
3523impl SequencedRecord {
3524 #[doc(hidden)]
3525 #[cfg(feature = "_hidden")]
3526 pub fn from_parts(
3527 seq_num: u64,
3528 timestamp: u64,
3529 headers: Vec<Header>,
3530 body: impl Into<Bytes>,
3531 ) -> Self {
3532 Self {
3533 seq_num,
3534 timestamp,
3535 body: body.into(),
3536 headers,
3537 }
3538 }
3539
3540 pub fn is_command_record(&self) -> bool {
3542 self.headers.len() == 1 && *self.headers[0].name == *b""
3543 }
3544}
3545
3546impl From<api::stream::proto::SequencedRecord> for SequencedRecord {
3547 fn from(value: api::stream::proto::SequencedRecord) -> Self {
3548 Self {
3549 seq_num: value.seq_num,
3550 body: value.body,
3551 headers: value.headers.into_iter().map(Into::into).collect(),
3552 timestamp: value.timestamp,
3553 }
3554 }
3555}
3556
3557metered_bytes_impl!(SequencedRecord);
3558
3559#[derive(Debug, Clone)]
3560#[non_exhaustive]
3561pub struct ReadBatch {
3564 pub records: Vec<SequencedRecord>,
3571 pub tail: Option<StreamPosition>,
3576}
3577
3578impl ReadBatch {
3579 pub(crate) fn from_api(batch: api::stream::proto::ReadBatch) -> Self {
3580 Self {
3581 records: batch.records.into_iter().map(Into::into).collect(),
3582 tail: batch.tail.map(Into::into),
3583 }
3584 }
3585}
3586
3587pub type Streaming<T> = Pin<Box<dyn Send + futures_core::Stream<Item = Result<T, S2Error>>>>;
3589
3590#[derive(Debug, Clone, thiserror::Error)]
3591pub enum AppendConditionFailed {
3593 #[error("fencing token mismatch, expected: {0}")]
3594 FencingTokenMismatch(FencingToken),
3596 #[error("sequence number mismatch, expected: {0}")]
3597 SeqNumMismatch(u64),
3599}
3600
3601impl From<api::stream::AppendConditionFailed> for AppendConditionFailed {
3602 fn from(value: api::stream::AppendConditionFailed) -> Self {
3603 match value {
3604 api::stream::AppendConditionFailed::FencingTokenMismatch(token) => {
3605 AppendConditionFailed::FencingTokenMismatch(FencingToken(token.to_string()))
3606 }
3607 api::stream::AppendConditionFailed::SeqNumMismatch(seq) => {
3608 AppendConditionFailed::SeqNumMismatch(seq)
3609 }
3610 }
3611 }
3612}
3613
3614#[derive(Debug, Clone, thiserror::Error)]
3615pub enum S2Error {
3617 #[error("{0}")]
3618 Client(String),
3620 #[error("malformed access token: {0}")]
3621 MalformedAccessToken(String),
3623 #[error(transparent)]
3624 Validation(#[from] ValidationError),
3626 #[error("{0}")]
3627 AppendConditionFailed(AppendConditionFailed),
3629 #[error("read from an unwritten position. current tail: {0}")]
3630 ReadUnwritten(StreamPosition),
3632 #[error("{0}")]
3633 Server(ErrorResponse),
3635}
3636
3637impl From<ApiError> for S2Error {
3638 fn from(err: ApiError) -> Self {
3639 match err {
3640 ApiError::ReadUnwritten(tail_response) => {
3641 Self::ReadUnwritten(tail_response.tail.into())
3642 }
3643 ApiError::AppendConditionFailed(condition_failed) => {
3644 Self::AppendConditionFailed(condition_failed.into())
3645 }
3646 ApiError::Server(_, response) => Self::Server(response.into()),
3647 ApiError::MalformedAccessToken(err) => Self::MalformedAccessToken(err),
3648 other => Self::Client(other.to_string()),
3649 }
3650 }
3651}
3652
3653#[derive(Debug, Clone, thiserror::Error)]
3654#[error("{code}: {message}")]
3655#[non_exhaustive]
3656pub struct ErrorResponse {
3658 pub code: String,
3660 pub message: String,
3662}
3663
3664impl From<ApiErrorResponse> for ErrorResponse {
3665 fn from(response: ApiErrorResponse) -> Self {
3666 Self {
3667 code: response.code,
3668 message: response.message,
3669 }
3670 }
3671}
3672
3673fn idempotency_token() -> String {
3674 uuid::Uuid::new_v4().simple().to_string()
3675}
3676
3677#[cfg(test)]
3678mod tests {
3679 use proptest::prelude::*;
3680 use rstest::rstest;
3681
3682 use super::*;
3683 use crate::api::ClientError;
3684
3685 type HeaderParts = (Vec<u8>, Vec<u8>);
3686 type AppendRecordParts = (Vec<u8>, Vec<HeaderParts>);
3687
3688 fn byte_vec_strategy(max_len: usize) -> impl Strategy<Value = Vec<u8>> {
3689 prop::collection::vec(any::<u8>(), 0..=max_len)
3690 }
3691
3692 fn header_parts_strategy() -> impl Strategy<Value = HeaderParts> {
3693 (byte_vec_strategy(32), byte_vec_strategy(64))
3694 }
3695
3696 fn string_strategy(max_chars: usize) -> impl Strategy<Value = String> {
3697 prop::collection::vec(any::<char>(), 0..=max_chars)
3698 .prop_map(|chars| chars.into_iter().collect())
3699 }
3700
3701 fn read_from_strategy() -> impl Strategy<Value = ReadFrom> {
3702 prop_oneof![
3703 any::<u64>().prop_map(ReadFrom::SeqNum),
3704 any::<u64>().prop_map(ReadFrom::Timestamp),
3705 any::<u64>().prop_map(ReadFrom::TailOffset),
3706 ]
3707 }
3708
3709 fn append_record_parts_strategy() -> impl Strategy<Value = AppendRecordParts> {
3710 (
3711 byte_vec_strategy(256),
3712 prop::collection::vec(header_parts_strategy(), 0..=16),
3713 )
3714 }
3715
3716 fn proto_stream_position_strategy() -> impl Strategy<Value = api::stream::proto::StreamPosition>
3717 {
3718 (any::<u64>(), any::<u64>()).prop_map(|(seq_num, timestamp)| {
3719 api::stream::proto::StreamPosition { seq_num, timestamp }
3720 })
3721 }
3722
3723 fn headers_from_parts(headers: &[HeaderParts]) -> Vec<Header> {
3724 headers
3725 .iter()
3726 .map(|(name, value)| Header::new(Bytes::from(name.clone()), Bytes::from(value.clone())))
3727 .collect()
3728 }
3729
3730 fn expected_metered_bytes(body: &[u8], headers: &[HeaderParts]) -> usize {
3731 8 + (2 * headers.len())
3732 + headers
3733 .iter()
3734 .map(|(name, value)| name.len() + value.len())
3735 .sum::<usize>()
3736 + body.len()
3737 }
3738
3739 #[test]
3742 fn s2_datetime_parse_valid_rfc3339() {
3743 let dt: S2DateTime = "2024-01-15T12:30:00Z".parse().unwrap();
3744 assert_eq!(dt.to_string(), "2024-01-15T12:30:00Z");
3745 }
3746
3747 #[test]
3748 fn s2_datetime_parse_with_offset() {
3749 let dt: S2DateTime = "2024-06-01T08:00:00+05:30".parse().unwrap();
3750 assert_eq!(dt.to_string(), "2024-06-01T08:00:00+05:30");
3751
3752 let offset_dt: time::OffsetDateTime = dt.into();
3753 assert_eq!(
3754 offset_dt.offset(),
3755 time::UtcOffset::from_hms(5, 30, 0).unwrap()
3756 );
3757 }
3758
3759 #[test]
3760 fn s2_datetime_parse_invalid() {
3761 let err = "not-a-date".parse::<S2DateTime>();
3762 assert!(err.is_err());
3763 }
3764
3765 #[test]
3766 fn s2_datetime_roundtrip_via_offset_datetime() {
3767 let odt = time::OffsetDateTime::from_unix_timestamp(1_700_000_000).unwrap();
3768 let dt = S2DateTime::try_from(odt).unwrap();
3769 let back: time::OffsetDateTime = dt.into();
3770 assert_eq!(odt, back);
3771 }
3772
3773 #[rstest]
3776 #[case::https_with_scheme("https://aws.s2.dev", Scheme::HTTPS)]
3777 #[case::http_with_scheme("http://localhost:8080", Scheme::HTTP)]
3778 #[case::default_https("aws.s2.dev", Scheme::HTTPS)]
3779 fn account_endpoint_parse(#[case] input: &str, #[case] expected_scheme: Scheme) {
3780 let ep: AccountEndpoint = input.parse().unwrap();
3781 assert_eq!(ep.scheme, expected_scheme);
3782 }
3783
3784 #[rstest]
3787 #[case::https_parent_zone("https://{basin}.b.s2.dev", Scheme::HTTPS, true)]
3788 #[case::http_direct("http://localhost:8080", Scheme::HTTP, false)]
3789 #[case::default_https_parent_zone("{basin}.b.s2.dev", Scheme::HTTPS, true)]
3790 fn basin_endpoint_parse(
3791 #[case] input: &str,
3792 #[case] expected_scheme: Scheme,
3793 #[case] expected_parent_zone: bool,
3794 ) {
3795 let ep: BasinEndpoint = input.parse().unwrap();
3796 assert_eq!(ep.scheme, expected_scheme);
3797 assert_eq!(
3798 matches!(ep.authority, BasinAuthority::ParentZone(_)),
3799 expected_parent_zone
3800 );
3801 }
3802
3803 #[test]
3806 fn s2_endpoints_new_requires_same_scheme() {
3807 let account: AccountEndpoint = "https://aws.s2.dev".parse().unwrap();
3808 let basin: BasinEndpoint = "http://localhost:8080".parse().unwrap();
3809 let err = S2Endpoints::new(account, basin);
3810 assert!(err.is_err());
3811 }
3812
3813 #[test]
3814 fn s2_endpoints_new_same_scheme_succeeds() {
3815 let account: AccountEndpoint = "https://aws.s2.dev".parse().unwrap();
3816 let basin: BasinEndpoint = "https://{basin}.b.s2.dev".parse().unwrap();
3817 let ep = S2Endpoints::new(account, basin).unwrap();
3818 assert_eq!(ep.scheme, Scheme::HTTPS);
3819 }
3820
3821 #[rstest]
3824 #[case::none(Compression::None, CompressionAlgorithm::None)]
3825 #[case::gzip(Compression::Gzip, CompressionAlgorithm::Gzip)]
3826 #[case::zstd(Compression::Zstd, CompressionAlgorithm::Zstd)]
3827 fn compression_conversion(#[case] sdk: Compression, #[case] api: CompressionAlgorithm) {
3828 assert_eq!(CompressionAlgorithm::from(sdk), api);
3829 }
3830
3831 #[test]
3834 fn retry_config_defaults() {
3835 let rc = RetryConfig::default();
3836 assert_eq!(rc.max_attempts.get(), 3);
3837 assert_eq!(rc.min_base_delay, Duration::from_millis(100));
3838 assert_eq!(rc.max_base_delay, Duration::from_secs(1));
3839 assert!(matches!(rc.append_retry_policy, AppendRetryPolicy::All));
3840 }
3841
3842 #[test]
3843 fn retry_config_max_retries() {
3844 let rc = RetryConfig::default();
3845 assert_eq!(rc.max_retries(), 2);
3846 }
3847
3848 #[test]
3851 fn s2_config_defaults() {
3852 let cfg = S2Config::new("test-token");
3853 assert_eq!(cfg.connection_timeout, Duration::from_secs(3));
3854 assert_eq!(cfg.request_timeout, Duration::from_secs(5));
3855 assert!(!cfg.insecure_skip_cert_verification);
3856 }
3857
3858 #[rstest]
3861 #[case::standard(StorageClass::Standard)]
3862 #[case::express(StorageClass::Express)]
3863 fn storage_class_roundtrip(#[case] sdk: StorageClass) {
3864 let api: api::config::StorageClass = sdk.into();
3865 let back: StorageClass = api.into();
3866 assert_eq!(back, sdk);
3867 }
3868
3869 #[rstest]
3872 #[case::age(RetentionPolicy::Age(3600))]
3873 #[case::infinite(RetentionPolicy::Infinite)]
3874 fn retention_policy_roundtrip(#[case] sdk: RetentionPolicy) {
3875 let api: api::config::RetentionPolicy = sdk.into();
3876 let back: RetentionPolicy = api.into();
3877 assert_eq!(back, sdk);
3878 }
3879
3880 #[rstest]
3883 #[case::client_prefer(
3884 TimestampingMode::ClientPrefer,
3885 api::config::TimestampingMode::ClientPrefer
3886 )]
3887 #[case::client_require(
3888 TimestampingMode::ClientRequire,
3889 api::config::TimestampingMode::ClientRequire
3890 )]
3891 #[case::arrival(TimestampingMode::Arrival, api::config::TimestampingMode::Arrival)]
3892 fn timestamping_mode_roundtrip(
3893 #[case] sdk: TimestampingMode,
3894 #[case] expected_api: api::config::TimestampingMode,
3895 ) {
3896 let converted: api::config::TimestampingMode = sdk.into();
3897 assert_eq!(converted, expected_api);
3898 let back: TimestampingMode = converted.into();
3899 assert_eq!(back, sdk);
3900 }
3901
3902 #[test]
3905 fn timestamping_config_roundtrip() {
3906 let sdk = TimestampingConfig {
3907 mode: Some(TimestampingMode::Arrival),
3908 uncapped: Some(true),
3909 };
3910 let api: api::config::TimestampingConfig = sdk.into();
3911 let back: TimestampingConfig = api.into();
3912 assert_eq!(back, sdk);
3913 }
3914
3915 #[test]
3918 fn delete_on_empty_config_roundtrip() {
3919 let sdk = DeleteOnEmptyConfig::new().with_min_age(Duration::from_secs(300));
3920 let api: api::config::DeleteOnEmptyConfig = sdk.into();
3921 let back: DeleteOnEmptyConfig = api.into();
3922 assert_eq!(back, sdk);
3923 }
3924
3925 #[test]
3928 fn stream_config_builder_and_roundtrip() {
3929 let sdk = StreamConfig::new()
3930 .with_storage_class(StorageClass::Express)
3931 .with_retention_policy(RetentionPolicy::Age(86400))
3932 .with_timestamping(TimestampingConfig {
3933 mode: Some(TimestampingMode::ClientPrefer),
3934 uncapped: None,
3935 })
3936 .with_delete_on_empty(DeleteOnEmptyConfig { min_age_secs: 60 });
3937 let api: api::config::StreamConfig = sdk.clone().into();
3938 let back: StreamConfig = api.into();
3939 assert_eq!(back, sdk);
3940 }
3941
3942 #[test]
3945 fn basin_config_builder_and_roundtrip() {
3946 let sdk = BasinConfig::new()
3947 .with_default_stream_config(
3948 StreamConfig::new().with_storage_class(StorageClass::Standard),
3949 )
3950 .with_create_stream_on_append(true)
3951 .with_create_stream_on_read(false);
3952 let api: api::config::BasinConfig = sdk.clone().into();
3953 let back: BasinConfig = api.into();
3954 assert_eq!(back, sdk);
3955 }
3956
3957 proptest! {
3960 #[test]
3961 fn fencing_token_parse_accepts_only_within_byte_limit(
3962 token in string_strategy(MAX_FENCING_TOKEN_LENGTH + 8),
3963 ) {
3964 let parsed = token.parse::<FencingToken>();
3965
3966 if token.len() <= MAX_FENCING_TOKEN_LENGTH {
3967 prop_assert_eq!(parsed.unwrap().to_string(), token);
3968 } else {
3969 prop_assert!(parsed.is_err());
3970 }
3971 }
3972 }
3973
3974 #[test]
3977 fn stream_position_display() {
3978 let pos = StreamPosition {
3979 seq_num: 42,
3980 timestamp: 1700000000,
3981 };
3982 assert_eq!(pos.to_string(), "seq_num=42, timestamp=1700000000");
3983 }
3984
3985 proptest! {
3986 #[test]
3987 fn stream_position_conversions_preserve_values(seq_num in any::<u64>(), timestamp in any::<u64>()) {
3988 let proto: StreamPosition = api::stream::proto::StreamPosition {
3989 seq_num,
3990 timestamp,
3991 }
3992 .into();
3993 prop_assert_eq!(proto.seq_num, seq_num);
3994 prop_assert_eq!(proto.timestamp, timestamp);
3995
3996 let api: StreamPosition = api::stream::StreamPosition {
3997 seq_num,
3998 timestamp,
3999 }
4000 .into();
4001 prop_assert_eq!(api.seq_num, seq_num);
4002 prop_assert_eq!(api.timestamp, timestamp);
4003 }
4004 }
4005
4006 proptest! {
4009 #[test]
4010 fn header_proto_roundtrip_preserves_binary_parts(
4011 name in byte_vec_strategy(64),
4012 value in byte_vec_strategy(128),
4013 ) {
4014 let header = Header::new(Bytes::from(name.clone()), Bytes::from(value.clone()));
4015 let proto: api::stream::proto::Header = header.into();
4016 let back: Header = proto.into();
4017
4018 prop_assert_eq!(back.name.as_ref(), name.as_slice());
4019 prop_assert_eq!(back.value.as_ref(), value.as_slice());
4020 }
4021 }
4022
4023 #[test]
4026 fn append_record_too_large() {
4027 let big_body = vec![0u8; RECORD_BATCH_MAX.bytes + 1];
4028 assert!(AppendRecord::new(big_body).is_err());
4029 }
4030
4031 proptest! {
4034 #[test]
4035 fn append_record_preserves_fields_and_metered_byte_formula(
4036 (body, headers) in append_record_parts_strategy(),
4037 timestamp in proptest::option::of(any::<u64>()),
4038 ) {
4039 let mut record = AppendRecord::new(body.clone())
4040 .unwrap()
4041 .with_headers(headers_from_parts(&headers))
4042 .unwrap();
4043 if let Some(timestamp) = timestamp {
4044 record = record.with_timestamp(timestamp);
4045 }
4046
4047 prop_assert_eq!(record.body(), body.as_slice());
4048 prop_assert_eq!(record.headers().len(), headers.len());
4049 prop_assert_eq!(record.timestamp(), timestamp);
4050 prop_assert_eq!(record.metered_bytes(), expected_metered_bytes(&body, &headers));
4051
4052 for (actual, (expected_name, expected_value)) in record.headers().iter().zip(headers.iter()) {
4053 prop_assert_eq!(actual.name.as_ref(), expected_name.as_slice());
4054 prop_assert_eq!(actual.value.as_ref(), expected_value.as_slice());
4055 }
4056 }
4057 }
4058
4059 #[test]
4062 fn append_record_batch_empty_is_err() {
4063 let result = AppendRecordBatch::try_from_iter(vec![]);
4064 assert!(result.is_err());
4065 }
4066
4067 #[test]
4068 fn append_record_batch_too_many_records() {
4069 let records: Vec<_> = (0..1001).map(|_| AppendRecord::new("x").unwrap()).collect();
4070 let result = AppendRecordBatch::try_from_iter(records);
4071 assert!(result.is_err());
4072 }
4073
4074 proptest! {
4075 #[test]
4076 fn append_record_batch_metered_bytes_is_sum_of_records(
4077 records in prop::collection::vec(append_record_parts_strategy(), 1..=32),
4078 ) {
4079 let expected = records
4080 .iter()
4081 .map(|(body, headers)| expected_metered_bytes(body, headers))
4082 .sum::<usize>();
4083 let records = records
4084 .into_iter()
4085 .map(|(body, headers)| {
4086 AppendRecord::new(body)
4087 .unwrap()
4088 .with_headers(headers_from_parts(&headers))
4089 .unwrap()
4090 })
4091 .collect::<Vec<_>>();
4092
4093 let batch = AppendRecordBatch::try_from_iter(records).unwrap();
4094 prop_assert_eq!(batch.metered_bytes(), expected);
4095 prop_assert_eq!(batch.iter().map(MeteredBytes::metered_bytes).sum::<usize>(), expected);
4096 }
4097 }
4098
4099 #[test]
4102 fn command_record_fence() {
4103 let token: FencingToken = "tok".parse().unwrap();
4104 let cmd = CommandRecord::fence(token);
4105 let record: AppendRecord = cmd.into();
4106 assert_eq!(record.headers().len(), 1);
4107 assert_eq!(record.headers()[0].name.as_ref(), b"");
4108 assert_eq!(record.headers()[0].value.as_ref(), b"fence");
4109 assert_eq!(record.body(), b"tok");
4110 }
4111
4112 #[test]
4113 fn command_record_trim() {
4114 let cmd = CommandRecord::trim(42);
4115 let record: AppendRecord = cmd.into();
4116 assert_eq!(record.headers().len(), 1);
4117 assert_eq!(record.headers()[0].value.as_ref(), b"trim");
4118 assert_eq!(record.body(), &42u64.to_be_bytes());
4119 }
4120
4121 #[rstest]
4124 #[case::command(vec![Header::new("", "fence")], true)]
4125 #[case::regular(vec![Header::new("key", "value")], false)]
4126 #[case::no_headers(vec![], false)]
4127 fn sequenced_record_command_detection(#[case] headers: Vec<Header>, #[case] expected: bool) {
4128 let record = SequencedRecord {
4129 seq_num: 0,
4130 body: Bytes::from("data"),
4131 headers,
4132 timestamp: 0,
4133 };
4134 assert_eq!(record.is_command_record(), expected);
4135 }
4136
4137 proptest! {
4140 #[test]
4141 fn read_start_to_api_sets_only_selected_position_field(
4142 from in read_from_strategy(),
4143 clamp_to_tail in any::<bool>(),
4144 ) {
4145 let (seq_num, timestamp, tail_offset) = match from {
4146 ReadFrom::SeqNum(value) => (Some(value), None, None),
4147 ReadFrom::Timestamp(value) => (None, Some(value), None),
4148 ReadFrom::TailOffset(value) => (None, None, Some(value)),
4149 };
4150 let api: api::stream::ReadStart = ReadStart::new()
4151 .with_from(from)
4152 .with_clamp_to_tail(clamp_to_tail)
4153 .into();
4154
4155 prop_assert_eq!(api.seq_num, seq_num);
4156 prop_assert_eq!(api.timestamp, timestamp);
4157 prop_assert_eq!(api.tail_offset, tail_offset);
4158 prop_assert_eq!(api.clamp, clamp_to_tail.then_some(true));
4159 }
4160 }
4161
4162 #[test]
4165 fn read_stop_to_api() {
4166 let stop = ReadStop::new()
4167 .with_limits(ReadLimits::new().with_count(50))
4168 .with_until(..1000)
4169 .with_wait(30);
4170 let api: api::stream::ReadEnd = stop.into();
4171 assert_eq!(api.count, Some(50));
4172 assert_eq!(api.until, Some(1000));
4173 assert_eq!(api.wait, Some(30));
4174 }
4175
4176 #[test]
4179 fn operation_roundtrip_all_variants() {
4180 let variants = [
4181 Operation::ListBasins,
4182 Operation::CreateBasin,
4183 Operation::GetBasinConfig,
4184 Operation::DeleteBasin,
4185 Operation::ReconfigureBasin,
4186 Operation::ListAccessTokens,
4187 Operation::IssueAccessToken,
4188 Operation::RevokeAccessToken,
4189 Operation::GetAccountMetrics,
4190 Operation::GetBasinMetrics,
4191 Operation::GetStreamMetrics,
4192 Operation::ListStreams,
4193 Operation::CreateStream,
4194 Operation::GetStreamConfig,
4195 Operation::DeleteStream,
4196 Operation::ReconfigureStream,
4197 Operation::CheckTail,
4198 Operation::Append,
4199 Operation::Read,
4200 Operation::Trim,
4201 Operation::Fence,
4202 Operation::ListLocations,
4203 Operation::GetDefaultLocation,
4204 Operation::SetDefaultLocation,
4205 ];
4206 for op in variants {
4207 let api_op: api::access::Operation = op.into();
4208 let back: Operation = api_op.into();
4209 assert_eq!(back, op);
4210 }
4211 }
4212
4213 #[test]
4216 fn metric_unit_conversion() {
4217 assert_eq!(
4218 MetricUnit::from(api::metrics::MetricUnit::Bytes),
4219 MetricUnit::Bytes
4220 );
4221 assert_eq!(
4222 MetricUnit::from(api::metrics::MetricUnit::Operations),
4223 MetricUnit::Operations
4224 );
4225 }
4226
4227 proptest! {
4230 #[test]
4231 fn append_ack_from_proto_preserves_present_positions_and_defaults_missing(
4232 start in proptest::option::of(proto_stream_position_strategy()),
4233 end in proptest::option::of(proto_stream_position_strategy()),
4234 tail in proptest::option::of(proto_stream_position_strategy()),
4235 ) {
4236 let expected_start = start.unwrap_or_default();
4237 let expected_end = end.unwrap_or_default();
4238 let expected_tail = tail.unwrap_or_default();
4239 let ack: AppendAck = api::stream::proto::AppendAck { start, end, tail }.into();
4240
4241 prop_assert_eq!(ack.start.seq_num, expected_start.seq_num);
4242 prop_assert_eq!(ack.start.timestamp, expected_start.timestamp);
4243 prop_assert_eq!(ack.end.seq_num, expected_end.seq_num);
4244 prop_assert_eq!(ack.end.timestamp, expected_end.timestamp);
4245 prop_assert_eq!(ack.tail.seq_num, expected_tail.seq_num);
4246 prop_assert_eq!(ack.tail.timestamp, expected_tail.timestamp);
4247 }
4248 }
4249
4250 #[test]
4253 fn read_batch_from_api() {
4254 let proto_batch = api::stream::proto::ReadBatch {
4255 records: vec![api::stream::proto::SequencedRecord {
4256 seq_num: 0,
4257 body: Bytes::from("hi"),
4258 headers: vec![api::stream::proto::Header {
4259 name: Bytes::from("k"),
4260 value: Bytes::from("v"),
4261 }],
4262 timestamp: 42,
4263 }],
4264 tail: Some(api::stream::proto::StreamPosition {
4265 seq_num: 1,
4266 timestamp: 42,
4267 }),
4268 };
4269 let batch = ReadBatch::from_api(proto_batch);
4270 assert_eq!(batch.records.len(), 1);
4271 assert_eq!(batch.records[0].seq_num, 0);
4272 assert_eq!(batch.records[0].timestamp, 42);
4273 assert_eq!(batch.records[0].body.as_ref(), b"hi");
4274 assert_eq!(batch.records[0].headers.len(), 1);
4275 assert_eq!(batch.records[0].headers[0].name.as_ref(), b"k");
4276 assert_eq!(batch.records[0].headers[0].value.as_ref(), b"v");
4277 assert_eq!(
4278 batch.tail,
4279 Some(StreamPosition {
4280 seq_num: 1,
4281 timestamp: 42,
4282 })
4283 );
4284 }
4285
4286 #[test]
4289 fn create_basin_input_to_api() {
4290 let name: BasinName = "test-basin-name".parse().unwrap();
4291 let input = CreateBasinInput::new(name.clone()).with_config(BasinConfig::new());
4292 let (req, token): (api::basin::CreateBasinRequest, String) = input.into();
4293 assert_eq!(req.basin, name);
4294 assert!(req.config.is_some());
4295 assert!(!token.is_empty());
4296 }
4297
4298 #[test]
4301 fn create_stream_input_to_api() {
4302 let name: StreamName = "my-stream".parse().unwrap();
4303 let input = CreateStreamInput::new(name.clone()).with_config(StreamConfig::new());
4304 let (req, token): (api::stream::CreateStreamRequest, String) = input.into();
4305 assert_eq!(req.stream, name);
4306 assert!(req.config.is_some());
4307 assert!(!token.is_empty());
4308 }
4309
4310 #[test]
4313 fn sequenced_record_from_proto() {
4314 let proto = api::stream::proto::SequencedRecord {
4315 seq_num: 99,
4316 body: Bytes::from("data"),
4317 headers: vec![api::stream::proto::Header {
4318 name: Bytes::from("k"),
4319 value: Bytes::from("v"),
4320 }],
4321 timestamp: 1234,
4322 };
4323 let record: SequencedRecord = proto.into();
4324 assert_eq!(record.seq_num, 99);
4325 assert_eq!(record.body.as_ref(), b"data");
4326 assert_eq!(record.headers.len(), 1);
4327 assert_eq!(record.headers[0].name.as_ref(), b"k");
4328 assert_eq!(record.headers[0].value.as_ref(), b"v");
4329 assert_eq!(record.timestamp, 1234);
4330 }
4331
4332 #[test]
4335 fn s2_error_from_api_error_client() {
4336 let err = ApiError::Client(ClientError::Others("client error".to_owned()));
4337 let s2_err: S2Error = err.into();
4338 assert!(matches!(s2_err, S2Error::Client(_)));
4339 }
4340
4341 #[test]
4344 fn error_response_from_api() {
4345 let api_resp = ApiErrorResponse {
4346 code: "not_found".to_string(),
4347 message: "basin not found".to_string(),
4348 };
4349 let resp: ErrorResponse = api_resp.into();
4350 assert_eq!(resp.code, "not_found");
4351 assert_eq!(resp.message, "basin not found");
4352 assert!(resp.to_string().contains("not_found"));
4353 }
4354}