1use std::{fmt, str::FromStr};
2
3use serde::{Deserialize, Serialize};
4
5#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
6#[serde(transparent)]
7pub struct ProviderId(String);
8
9impl ProviderId {
10 pub fn new(value: impl Into<String>) -> Result<Self, ProviderIdError> {
11 let value = value.into();
12 if valid_id(&value) {
13 Ok(Self(value))
14 } else {
15 Err(ProviderIdError(value))
16 }
17 }
18
19 pub fn as_str(&self) -> &str {
20 &self.0
21 }
22
23 pub fn into_string(self) -> String {
24 self.0
25 }
26}
27
28impl fmt::Display for ProviderId {
29 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
30 f.write_str(&self.0)
31 }
32}
33
34impl FromStr for ProviderId {
35 type Err = ProviderIdError;
36
37 fn from_str(value: &str) -> Result<Self, Self::Err> {
38 Self::new(value)
39 }
40}
41
42#[derive(Debug, Clone, thiserror::Error, PartialEq, Eq)]
43#[error("invalid provider id `{0}`; expected a lowercase provider name")]
44pub struct ProviderIdError(String);
45
46fn valid_id(value: &str) -> bool {
47 let mut chars = value.chars();
48 matches!(chars.next(), Some('a'..='z'))
49 && chars.all(|ch| ch.is_ascii_lowercase() || ch.is_ascii_digit() || matches!(ch, '-' | '_'))
50 && !value.ends_with(['-', '_'])
51 && !value.contains("--")
52 && !value.contains("__")
53 && !value.contains("-_")
54 && !value.contains("_-")
55}