s2_common/types/
basin.rs

1use std::{marker::PhantomData, ops::Deref, str::FromStr};
2
3use compact_str::{CompactString, ToCompactString};
4
5use super::{
6    ValidationError,
7    strings::{NameProps, PrefixProps, StartAfterProps, StrProps},
8};
9use crate::{caps, types::resources::ListItemsRequest};
10
11pub static BASIN_HEADER: http::HeaderName = http::HeaderName::from_static("s2-basin");
12
13#[derive(Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
14#[cfg_attr(
15    feature = "rkyv",
16    derive(rkyv::Archive, rkyv::Serialize, rkyv::Deserialize)
17)]
18pub struct BasinNameStr<T: StrProps>(CompactString, PhantomData<T>);
19
20impl<T: StrProps> BasinNameStr<T> {
21    fn validate_str(name: &str) -> Result<(), ValidationError> {
22        if name.len() > caps::MAX_BASIN_NAME_LEN {
23            return Err(format!(
24                "basin {} must not exceed {} bytes in length",
25                T::FIELD_NAME,
26                caps::MAX_BASIN_NAME_LEN
27            )
28            .into());
29        }
30
31        if !T::IS_PREFIX && name.len() < caps::MIN_BASIN_NAME_LEN {
32            return Err(format!(
33                "basin {} should be at least {} bytes in length",
34                T::FIELD_NAME,
35                caps::MIN_BASIN_NAME_LEN
36            )
37            .into());
38        }
39
40        let mut chars = name.chars();
41
42        let Some(first_char) = chars.next() else {
43            return Ok(());
44        };
45
46        if !first_char.is_ascii_lowercase() && !first_char.is_ascii_digit() {
47            return Err(format!(
48                "basin {} must begin with a lowercase letter or number",
49                T::FIELD_NAME
50            )
51            .into());
52        }
53
54        if !T::IS_PREFIX
55            && let Some(last_char) = chars.next_back()
56            && !last_char.is_ascii_lowercase()
57            && !last_char.is_ascii_digit()
58        {
59            return Err(format!(
60                "basin {} must end with a lowercase letter or number",
61                T::FIELD_NAME
62            )
63            .into());
64        }
65
66        if chars.any(|c| !c.is_ascii_lowercase() && !c.is_ascii_digit() && c != '-') {
67            return Err(format!(
68                "basin {} must comprise lowercase letters, numbers, and hyphens",
69                T::FIELD_NAME
70            )
71            .into());
72        }
73
74        Ok(())
75    }
76}
77
78#[cfg(feature = "utoipa")]
79impl<T> utoipa::PartialSchema for BasinNameStr<T>
80where
81    T: StrProps,
82{
83    fn schema() -> utoipa::openapi::RefOr<utoipa::openapi::schema::Schema> {
84        utoipa::openapi::Object::builder()
85            .schema_type(utoipa::openapi::Type::String)
86            .min_length((!T::IS_PREFIX).then_some(caps::MIN_BASIN_NAME_LEN))
87            .max_length(Some(caps::MAX_BASIN_NAME_LEN))
88            .into()
89    }
90}
91
92#[cfg(feature = "utoipa")]
93impl<T> utoipa::ToSchema for BasinNameStr<T> where T: StrProps {}
94
95impl<T: StrProps> serde::Serialize for BasinNameStr<T> {
96    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
97    where
98        S: serde::Serializer,
99    {
100        serializer.serialize_str(&self.0)
101    }
102}
103
104impl<'de, T: StrProps> serde::Deserialize<'de> for BasinNameStr<T> {
105    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
106    where
107        D: serde::Deserializer<'de>,
108    {
109        let s = CompactString::deserialize(deserializer)?;
110        s.try_into().map_err(serde::de::Error::custom)
111    }
112}
113
114impl<T: StrProps> AsRef<str> for BasinNameStr<T> {
115    fn as_ref(&self) -> &str {
116        &self.0
117    }
118}
119
120impl<T: StrProps> Deref for BasinNameStr<T> {
121    type Target = str;
122
123    fn deref(&self) -> &Self::Target {
124        &self.0
125    }
126}
127
128impl<T: StrProps> TryFrom<CompactString> for BasinNameStr<T> {
129    type Error = ValidationError;
130
131    fn try_from(name: CompactString) -> Result<Self, Self::Error> {
132        Self::validate_str(&name)?;
133        Ok(Self(name, PhantomData))
134    }
135}
136
137impl<T: StrProps> FromStr for BasinNameStr<T> {
138    type Err = ValidationError;
139
140    fn from_str(s: &str) -> Result<Self, Self::Err> {
141        Self::validate_str(s)?;
142        Ok(Self(s.to_compact_string(), PhantomData))
143    }
144}
145
146impl<T: StrProps> std::fmt::Debug for BasinNameStr<T> {
147    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
148        f.write_str(&self.0)
149    }
150}
151
152impl<T: StrProps> std::fmt::Display for BasinNameStr<T> {
153    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
154        f.write_str(&self.0)
155    }
156}
157
158impl<T: StrProps> From<BasinNameStr<T>> for CompactString {
159    fn from(value: BasinNameStr<T>) -> Self {
160        value.0
161    }
162}
163
164pub type BasinName = BasinNameStr<NameProps>;
165
166pub type BasinNamePrefix = BasinNameStr<PrefixProps>;
167
168impl Default for BasinNamePrefix {
169    fn default() -> Self {
170        BasinNameStr(CompactString::default(), PhantomData)
171    }
172}
173
174impl From<BasinName> for BasinNamePrefix {
175    fn from(value: BasinName) -> Self {
176        Self(value.0, PhantomData)
177    }
178}
179
180pub type BasinNameStartAfter = BasinNameStr<StartAfterProps>;
181
182impl Default for BasinNameStartAfter {
183    fn default() -> Self {
184        BasinNameStr(CompactString::default(), PhantomData)
185    }
186}
187
188impl From<BasinName> for BasinNameStartAfter {
189    fn from(value: BasinName) -> Self {
190        Self(value.0, PhantomData)
191    }
192}
193
194impl crate::http::ParseableHeader for BasinName {
195    fn name() -> &'static http::HeaderName {
196        &BASIN_HEADER
197    }
198}
199
200pub type ListBasinsRequest = ListItemsRequest<BasinNamePrefix, BasinNameStartAfter>;
201
202#[derive(Debug, Clone, Copy)]
203pub enum BasinState {
204    Active,
205    Creating,
206    Deleting,
207}
208
209#[derive(Debug, strum::Display, Clone, Copy, PartialEq, Eq)]
210pub enum BasinScope {
211    #[strum(serialize = "aws:us-east-1")]
212    AwsUsEast1,
213}
214
215#[derive(Debug, Clone)]
216pub struct BasinInfo {
217    pub name: BasinName,
218    pub scope: Option<BasinScope>,
219    pub state: BasinState,
220}
221
222#[cfg(test)]
223mod test {
224    use rstest::rstest;
225
226    use super::{BasinNameStr, NameProps, PrefixProps, StartAfterProps};
227
228    #[rstest]
229    #[case::min_len("abcdefgh".to_owned())]
230    #[case::starts_with_digit("1abcdefg".to_owned())]
231    #[case::contains_hyphen("abcd-efg".to_owned())]
232    #[case::max_len("a".repeat(crate::caps::MAX_BASIN_NAME_LEN))]
233    fn validate_name_ok(#[case] name: String) {
234        assert_eq!(BasinNameStr::<NameProps>::validate_str(&name), Ok(()));
235    }
236
237    #[rstest]
238    #[case::too_long("a".repeat(crate::caps::MAX_BASIN_NAME_LEN + 1))]
239    #[case::too_short("abcdefg".to_owned())]
240    #[case::empty("".to_owned())]
241    #[case::invalid_first_char("Abcdefgh".to_owned())]
242    #[case::invalid_last_char("abcdefg-".to_owned())]
243    #[case::invalid_characters("abcd_efg".to_owned())]
244    fn validate_name_err(#[case] name: String) {
245        BasinNameStr::<NameProps>::validate_str(&name).expect_err("expected validation error");
246    }
247
248    #[rstest]
249    #[case::empty("".to_owned())]
250    #[case::single_char("a".to_owned())]
251    #[case::trailing_hyphen("abcdefg-".to_owned())]
252    #[case::max_len("a".repeat(crate::caps::MAX_BASIN_NAME_LEN))]
253    fn validate_prefix_ok(#[case] prefix: String) {
254        assert_eq!(BasinNameStr::<PrefixProps>::validate_str(&prefix), Ok(()));
255    }
256
257    #[rstest]
258    #[case::too_long("a".repeat(crate::caps::MAX_BASIN_NAME_LEN + 1))]
259    #[case::invalid_first_char("-abc".to_owned())]
260    #[case::invalid_characters("ab_cd".to_owned())]
261    fn validate_prefix_err(#[case] prefix: String) {
262        BasinNameStr::<PrefixProps>::validate_str(&prefix).expect_err("expected validation error");
263    }
264
265    #[rstest]
266    #[case::empty("".to_owned())]
267    #[case::single_char("a".to_owned())]
268    #[case::trailing_hyphen("abcdefg-".to_owned())]
269    fn validate_start_after_ok(#[case] start_after: String) {
270        assert_eq!(
271            BasinNameStr::<StartAfterProps>::validate_str(&start_after),
272            Ok(())
273        );
274    }
275
276    #[rstest]
277    #[case::too_long("a".repeat(crate::caps::MAX_BASIN_NAME_LEN + 1))]
278    #[case::invalid_first_char("-abc".to_owned())]
279    #[case::invalid_characters("ab_cd".to_owned())]
280    fn validate_start_after_err(#[case] start_after: String) {
281        BasinNameStr::<StartAfterProps>::validate_str(&start_after)
282            .expect_err("expected validation error");
283    }
284}