drawbridge_type/repository/
name.rs

1// SPDX-License-Identifier: Apache-2.0
2
3use 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/// A repository name
12#[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}