1use core::fmt;
4
5pub const MAX_PROVIDER_ID_BYTES: usize = 63;
7
8pub const MAX_SERVICE_ID_BYTES: usize = 63;
10
11#[derive(Clone, Copy, Debug, Eq, PartialEq)]
13pub enum IdentityError {
14 Empty,
16 TooLong,
18 InvalidByte,
20 InvalidBoundary,
22 RepeatedSeparator,
24}
25
26impl fmt::Display for IdentityError {
27 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
28 formatter.write_str(match self {
29 Self::Empty => "provider or service identifier is empty",
30 Self::TooLong => "provider or service identifier is too long",
31 Self::InvalidByte => "provider or service identifier contains an invalid byte",
32 Self::InvalidBoundary => "provider or service identifier has an invalid boundary",
33 Self::RepeatedSeparator => {
34 "provider or service identifier contains repeated separators"
35 }
36 })
37 }
38}
39
40impl core::error::Error for IdentityError {}
41
42#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
44pub struct ProviderId(&'static str);
45
46impl ProviderId {
47 pub const fn new(value: &'static str) -> Result<Self, IdentityError> {
53 match validate(value, MAX_PROVIDER_ID_BYTES) {
54 Ok(()) => Ok(Self(value)),
55 Err(error) => Err(error),
56 }
57 }
58
59 #[must_use]
61 pub const fn as_str(self) -> &'static str {
62 self.0
63 }
64}
65
66#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
68pub struct ServiceId(&'static str);
69
70impl ServiceId {
71 pub const fn new(value: &'static str) -> Result<Self, IdentityError> {
76 match validate(value, MAX_SERVICE_ID_BYTES) {
77 Ok(()) => Ok(Self(value)),
78 Err(error) => Err(error),
79 }
80 }
81
82 #[must_use]
84 pub const fn as_str(self) -> &'static str {
85 self.0
86 }
87}
88
89pub trait ProviderMarker {
114 const ID: ProviderId;
116}
117
118pub trait ServiceMarker {
133 type Provider: ProviderMarker;
135
136 const ID: ServiceId;
138}
139
140#[doc(hidden)]
141pub const fn validate(value: &str, max_bytes: usize) -> Result<(), IdentityError> {
142 if value.is_empty() {
143 return Err(IdentityError::Empty);
144 }
145 if value.len() > max_bytes {
146 return Err(IdentityError::TooLong);
147 }
148 let mut remaining = value.as_bytes();
149 let mut first = true;
150 let mut previous_separator = false;
151 while let Some((byte, tail)) = remaining.split_first() {
152 let separator = *byte == b'-';
153 if !byte.is_ascii_lowercase() && !byte.is_ascii_digit() && !separator {
154 return Err(IdentityError::InvalidByte);
155 }
156 if separator && first {
157 return Err(IdentityError::InvalidBoundary);
158 }
159 if separator && previous_separator {
160 return Err(IdentityError::RepeatedSeparator);
161 }
162 previous_separator = separator;
163 first = false;
164 remaining = tail;
165 }
166 if previous_separator {
167 return Err(IdentityError::InvalidBoundary);
168 }
169 Ok(())
170}
171
172#[macro_export]
184macro_rules! provider_id {
185 ($value:literal) => {{
186 const VALUE: $crate::ProviderId = match $crate::ProviderId::new($value) {
187 Ok(value) => value,
188 Err(_) => panic!("invalid provider identifier literal"),
189 };
190 VALUE
191 }};
192}
193
194#[macro_export]
206macro_rules! service_id {
207 ($value:literal) => {{
208 const VALUE: $crate::ServiceId = match $crate::ServiceId::new($value) {
209 Ok(value) => value,
210 Err(_) => panic!("invalid service identifier literal"),
211 };
212 VALUE
213 }};
214}
215
216#[cfg(test)]
217mod tests {
218 use super::{
219 IdentityError, MAX_PROVIDER_ID_BYTES, MAX_SERVICE_ID_BYTES, ProviderId, ProviderMarker,
220 ServiceId, ServiceMarker,
221 };
222
223 enum Example {}
224
225 impl ProviderMarker for Example {
226 const ID: ProviderId = provider_id!("example");
227 }
228
229 enum Compute {}
230
231 impl ServiceMarker for Compute {
232 type Provider = Example;
233 const ID: ServiceId = service_id!("compute-v2");
234 }
235
236 #[test]
237 fn markers_define_external_namespaces_without_core_registration() {
238 assert_eq!(Example::ID.as_str(), "example");
239 assert_eq!(Compute::ID.as_str(), "compute-v2");
240 assert_eq!(
241 <<Compute as ServiceMarker>::Provider as ProviderMarker>::ID,
242 Example::ID
243 );
244 }
245
246 #[test]
247 fn accepts_canonical_bounded_identifiers() {
248 assert_eq!(
249 ProviderId::new("provider-9").map(ProviderId::as_str),
250 Ok("provider-9")
251 );
252 assert_eq!(
253 ServiceId::new("object-storage").map(ServiceId::as_str),
254 Ok("object-storage")
255 );
256 assert!(ProviderId::new("a").is_ok());
257 }
258
259 #[test]
260 fn literal_macros_validate_inside_function_bodies() {
261 let provider = provider_id!("example-provider");
262 let service = service_id!("object-storage");
263 assert_eq!(provider.as_str(), "example-provider");
264 assert_eq!(service.as_str(), "object-storage");
265 }
266
267 #[test]
268 fn rejects_every_noncanonical_identifier_class() {
269 for value in [
270 "",
271 "-cloud",
272 "cloud-",
273 "cloud--api",
274 "Cloud",
275 "cloud_api",
276 "c loud",
277 "é",
278 ] {
279 assert!(ProviderId::new(value).is_err(), "{value}");
280 assert!(ServiceId::new(value).is_err(), "{value}");
281 }
282 assert_eq!(ProviderId::new(""), Err(IdentityError::Empty));
283 assert_eq!(
284 ProviderId::new("cloud--api"),
285 Err(IdentityError::RepeatedSeparator)
286 );
287 assert_eq!(
288 ServiceId::new("-storage"),
289 Err(IdentityError::InvalidBoundary)
290 );
291 }
292
293 #[test]
294 fn enforces_domain_length_limits_before_scanning() {
295 const PROVIDER_MAX: &str =
296 "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa";
297 const PROVIDER_LONG: &str =
298 "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa";
299 const SERVICE_MAX: &str = "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb";
300 const SERVICE_LONG: &str =
301 "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb";
302 assert_eq!(PROVIDER_MAX.len(), MAX_PROVIDER_ID_BYTES);
303 assert_eq!(SERVICE_MAX.len(), MAX_SERVICE_ID_BYTES);
304 assert!(ProviderId::new(PROVIDER_MAX).is_ok());
305 assert!(ServiceId::new(SERVICE_MAX).is_ok());
306 assert_eq!(ProviderId::new(PROVIDER_LONG), Err(IdentityError::TooLong));
307 assert_eq!(ServiceId::new(SERVICE_LONG), Err(IdentityError::TooLong));
308 }
309}