s2_common/types/
location.rs1use std::{ops::Deref, str::FromStr};
2
3use compact_str::{CompactString, ToCompactString};
4
5use super::ValidationError;
6use crate::caps;
7
8fn validate_location_str(field_name: &str, location: &str) -> Result<(), ValidationError> {
9 if location.chars().count() > caps::MAX_LOCATION_NAME_LEN {
10 return Err(format!(
11 "location {field_name} must be at most {} characters in length",
12 caps::MAX_LOCATION_NAME_LEN
13 )
14 .into());
15 }
16
17 if location
18 .chars()
19 .any(|c| !c.is_ascii_alphanumeric() && c != ':' && c != '-' && c != '.')
20 {
21 return Err(format!(
22 "location {field_name} must comprise ASCII letters, numbers, colons, hyphens, and periods"
23 )
24 .into());
25 }
26
27 Ok(())
28}
29
30#[derive(Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
31#[cfg_attr(
32 feature = "rkyv",
33 derive(rkyv::Archive, rkyv::Serialize, rkyv::Deserialize)
34)]
35pub struct LocationName(CompactString);
36
37impl LocationName {
38 fn validate_str(location: &str) -> Result<(), ValidationError> {
39 if location.is_empty() {
40 return Err("location name must be at least 1 character in length".into());
41 }
42
43 validate_location_str("name", location)
44 }
45}
46
47#[cfg(feature = "utoipa")]
48impl utoipa::PartialSchema for LocationName {
49 fn schema() -> utoipa::openapi::RefOr<utoipa::openapi::schema::Schema> {
50 utoipa::openapi::Object::builder()
51 .schema_type(utoipa::openapi::Type::String)
52 .min_length(Some(1))
53 .max_length(Some(caps::MAX_LOCATION_NAME_LEN))
54 .into()
55 }
56}
57
58#[cfg(feature = "utoipa")]
59impl utoipa::ToSchema for LocationName {}
60
61impl serde::Serialize for LocationName {
62 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
63 where
64 S: serde::Serializer,
65 {
66 serializer.serialize_str(&self.0)
67 }
68}
69
70impl<'de> serde::Deserialize<'de> for LocationName {
71 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
72 where
73 D: serde::Deserializer<'de>,
74 {
75 let s = CompactString::deserialize(deserializer)?;
76 s.try_into().map_err(serde::de::Error::custom)
77 }
78}
79
80impl AsRef<str> for LocationName {
81 fn as_ref(&self) -> &str {
82 &self.0
83 }
84}
85
86impl Deref for LocationName {
87 type Target = str;
88
89 fn deref(&self) -> &Self::Target {
90 &self.0
91 }
92}
93
94impl TryFrom<CompactString> for LocationName {
95 type Error = ValidationError;
96
97 fn try_from(location: CompactString) -> Result<Self, Self::Error> {
98 Self::validate_str(&location)?;
99 Ok(Self(location))
100 }
101}
102
103impl TryFrom<String> for LocationName {
104 type Error = ValidationError;
105
106 fn try_from(location: String) -> Result<Self, Self::Error> {
107 location.to_compact_string().try_into()
108 }
109}
110
111impl TryFrom<&str> for LocationName {
112 type Error = ValidationError;
113
114 fn try_from(location: &str) -> Result<Self, Self::Error> {
115 location.to_compact_string().try_into()
116 }
117}
118
119impl FromStr for LocationName {
120 type Err = ValidationError;
121
122 fn from_str(s: &str) -> Result<Self, Self::Err> {
123 s.try_into()
124 }
125}
126
127impl std::fmt::Debug for LocationName {
128 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
129 f.write_str(&self.0)
130 }
131}
132
133impl std::fmt::Display for LocationName {
134 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
135 f.write_str(&self.0)
136 }
137}
138
139impl From<LocationName> for CompactString {
140 fn from(value: LocationName) -> Self {
141 value.0
142 }
143}
144
145#[derive(Debug, Clone)]
146pub struct LocationInfo {
147 pub name: LocationName,
148 pub is_private: bool,
149}
150
151#[cfg(test)]
152mod test {
153 use rstest::rstest;
154
155 use super::LocationName;
156
157 #[rstest]
158 #[case::single_char("a".to_owned())]
159 #[case::aws_region("aws:us-east-1".to_owned())]
160 #[case::uppercase_and_period("cloud:US-West-2.edge".to_owned())]
161 #[case::max_len("a".repeat(crate::caps::MAX_LOCATION_NAME_LEN))]
162 fn validate_name_ok(#[case] location: String) {
163 assert_eq!(
164 location.parse::<LocationName>().as_deref(),
165 Ok(location.as_str())
166 );
167 }
168
169 #[rstest]
170 #[case::empty("".to_owned())]
171 #[case::too_long("a".repeat(crate::caps::MAX_LOCATION_NAME_LEN + 1))]
172 #[case::underscore("aws:us_east-1".to_owned())]
173 #[case::slash("aws/us-east-1".to_owned())]
174 #[case::space("aws:us east-1".to_owned())]
175 #[case::multibyte("aws:é".to_owned())]
176 fn validate_name_err(#[case] location: String) {
177 location
178 .parse::<LocationName>()
179 .expect_err("expected validation error");
180 }
181}