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}