firestore_path/
project_id.rs

1use crate::{error::ErrorKind, Error};
2
3/// A project id.
4///
5/// # Limit
6///
7/// <https://cloud.google.com/resource-manager/docs/creating-managing-projects>
8///
9/// > - Must be 6 to 30 characters in length.
10/// > - Can only contain lowercase letters, numbers, and hyphens.
11/// > - Must start with a letter.
12/// > - Cannot end with a hyphen.
13/// > - Cannot be in use or previously used; this includes deleted projects.
14/// > - Cannot contain restricted strings, such as google, null, undefined, and ssl.
15///
16/// # Examples
17///
18/// ```rust
19/// # fn main() -> anyhow::Result<()> {
20/// use firestore_path::ProjectId;
21/// use std::str::FromStr;
22///
23/// let project_id = ProjectId::from_str("my-project")?;
24/// assert_eq!(project_id.as_ref(), "my-project");
25/// assert_eq!(project_id.to_string(), "my-project");
26/// #     Ok(())
27/// # }
28/// ```
29///
30#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
31pub struct ProjectId(String);
32
33impl std::convert::AsRef<str> for ProjectId {
34    fn as_ref(&self) -> &str {
35        self.0.as_ref()
36    }
37}
38
39impl std::convert::TryFrom<&str> for ProjectId {
40    type Error = Error;
41
42    fn try_from(s: &str) -> Result<Self, Self::Error> {
43        Self::try_from(s.to_string())
44    }
45}
46
47impl std::convert::TryFrom<String> for ProjectId {
48    type Error = Error;
49
50    fn try_from(s: String) -> Result<Self, Self::Error> {
51        // <https://cloud.google.com/resource-manager/docs/creating-managing-projects>
52
53        if !(6..=30).contains(&s.len()) {
54            return Err(Error::from(ErrorKind::LengthOutOfBounds));
55        }
56
57        if !s
58            .chars()
59            .all(|c| c.is_ascii_lowercase() || c.is_ascii_digit() || c == '-')
60        {
61            return Err(Error::from(ErrorKind::ContainsInvalidCharacter));
62        }
63
64        let first_char = s.chars().next().expect("already length checked");
65        if !first_char.is_ascii_lowercase() {
66            return Err(Error::from(ErrorKind::StartsWithNonLetter));
67        }
68
69        let last_char = s.chars().next_back().expect("already length checked");
70        if last_char == '-' {
71            return Err(Error::from(ErrorKind::EndsWithHyphen));
72        }
73
74        if s.contains("google")
75            || s.contains("null")
76            || s.contains("undefined")
77            || s.contains("ssl")
78        {
79            return Err(Error::from(ErrorKind::MatchesReservedIdPattern));
80        }
81
82        Ok(Self(s))
83    }
84}
85
86impl std::fmt::Display for ProjectId {
87    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
88        self.0.fmt(f)
89    }
90}
91
92impl std::str::FromStr for ProjectId {
93    type Err = Error;
94
95    fn from_str(s: &str) -> Result<Self, Self::Err> {
96        Self::try_from(s)
97    }
98}
99
100#[cfg(test)]
101mod tests {
102    use std::str::FromStr;
103
104    use super::*;
105
106    #[test]
107    fn test() -> anyhow::Result<()> {
108        let s = "my-project";
109        let project_id = ProjectId::from_str(s)?;
110        assert_eq!(project_id.to_string(), s);
111
112        assert_eq!(project_id.as_ref(), s);
113        Ok(())
114    }
115
116    #[test]
117    fn test_impl_from_str_and_impl_try_from_string() -> anyhow::Result<()> {
118        for (s, expected) in [
119            ("x".repeat(5).as_ref(), false),
120            ("x".repeat(6).as_ref(), true),
121            ("x".repeat(30).as_ref(), true),
122            ("x".repeat(31).as_ref(), false),
123            ("chat/rooms", false),
124            ("xxxxxx", true),
125            ("x-xxxx", true),
126            ("x0xxxx", true),
127            ("xAxxxx", false),
128            ("0xxxxx", false),
129            ("xxxxx0", true),
130            ("xxxxx-", false),
131            ("xgoogle", false),
132            ("xnull", false),
133            ("xundefined", false),
134            ("xssl", false),
135        ] {
136            assert_eq!(ProjectId::from_str(s).is_ok(), expected);
137            assert_eq!(ProjectId::try_from(s).is_ok(), expected);
138            assert_eq!(ProjectId::try_from(s.to_string()).is_ok(), expected);
139            if expected {
140                assert_eq!(ProjectId::from_str(s)?, ProjectId::try_from(s)?);
141                assert_eq!(ProjectId::from_str(s)?, ProjectId::try_from(s.to_string())?);
142                assert_eq!(ProjectId::from_str(s)?.to_string(), s);
143            }
144        }
145        Ok(())
146    }
147}