firestore_path/
project_id.rs1use crate::{error::ErrorKind, Error};
2
3#[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 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}