drawbridge_type/repository/
name.rs1use std::fmt::Display;
4use std::ops::Deref;
5use std::str::FromStr;
6
7use anyhow::bail;
8use serde::de::Error;
9use serde::{Deserialize, Deserializer, Serialize};
10
11#[derive(Clone, Debug, Eq, Hash, PartialEq, Serialize)]
13#[repr(transparent)]
14#[serde(transparent)]
15pub struct Name(String);
16
17impl Name {
18 #[inline]
19 fn validate(s: impl AsRef<str>) -> anyhow::Result<()> {
20 let s = s.as_ref();
21 if s.is_empty() {
22 bail!("empty repository name")
23 } else if s
24 .find(|c| !matches!(c, '0'..='9' | 'a'..='z' | 'A'..='Z' | '-'))
25 .is_some()
26 {
27 bail!("invalid characters in repository name")
28 } else {
29 Ok(())
30 }
31 }
32}
33
34impl AsRef<str> for Name {
35 fn as_ref(&self) -> &str {
36 &self.0
37 }
38}
39
40impl AsRef<String> for Name {
41 fn as_ref(&self) -> &String {
42 &self.0
43 }
44}
45
46impl Deref for Name {
47 type Target = String;
48
49 fn deref(&self) -> &Self::Target {
50 &self.0
51 }
52}
53
54impl<'de> Deserialize<'de> for Name {
55 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
56 where
57 D: Deserializer<'de>,
58 {
59 let name = String::deserialize(deserializer)?;
60 name.try_into().map_err(D::Error::custom)
61 }
62}
63
64impl Display for Name {
65 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
66 write!(f, "{}", self.0)
67 }
68}
69
70impl FromStr for Name {
71 type Err = anyhow::Error;
72
73 #[inline]
74 fn from_str(s: &str) -> Result<Self, Self::Err> {
75 Self::validate(s).map(|()| Self(s.into()))
76 }
77}
78
79impl TryFrom<String> for Name {
80 type Error = anyhow::Error;
81
82 #[inline]
83 fn try_from(s: String) -> Result<Self, Self::Error> {
84 Self::validate(&s).map(|()| Self(s))
85 }
86}
87
88#[cfg(test)]
89mod tests {
90 use super::*;
91
92 #[test]
93 fn from_str() {
94 assert!("".parse::<Name>().is_err());
95 assert!(" ".parse::<Name>().is_err());
96 assert!("/".parse::<Name>().is_err());
97 assert!("/name".parse::<Name>().is_err());
98 assert!("name/".parse::<Name>().is_err());
99 assert!("/name/".parse::<Name>().is_err());
100 assert!("group//name".parse::<Name>().is_err());
101 assert!("group/subgroup///name".parse::<Name>().is_err());
102 assert!("group/subg%roup/name".parse::<Name>().is_err());
103 assert!("group/subgяoup/name".parse::<Name>().is_err());
104 assert!("group /subgroup/name".parse::<Name>().is_err());
105 assert!("group/subgr☣up/name".parse::<Name>().is_err());
106 assert!("gr.oup/subgroup/name".parse::<Name>().is_err());
107 assert!("group/name".parse::<Name>().is_err());
108 assert!("group/subgroup/name".parse::<Name>().is_err());
109 assert!("gr0uP/subgr0up/-n4mE".parse::<Name>().is_err());
110
111 assert_eq!("name".parse::<Name>().unwrap(), Name("name".into()));
112 assert_eq!("-n4M3".parse::<Name>().unwrap(), Name("-n4M3".into()));
113 }
114}