firestore_path/
collection_id.rs

1use crate::{error::ErrorKind, Error};
2
3/// A collection id.
4///
5/// # Limit
6///
7/// <https://firebase.google.com/docs/firestore/quotas#collections_documents_and_fields>
8///
9/// > - Must be valid UTF-8 characters
10/// > - Must be no longer than 1,500 bytes
11/// > - Cannot contain a forward slash (/)
12/// > - Cannot solely consist of a single period (.) or double periods (..)
13/// > - Cannot match the regular expression __.*__
14///
15/// # Examples
16///
17/// ```rust
18/// # fn main() -> anyhow::Result<()> {
19/// use firestore_path::CollectionId;
20/// use std::str::FromStr;
21///
22/// let collection_id = CollectionId::from_str("chatrooms")?;
23/// assert_eq!(collection_id.as_ref(), "chatrooms");
24/// assert_eq!(collection_id.to_string(), "chatrooms");
25/// #     Ok(())
26/// # }
27/// ```
28///
29#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
30pub struct CollectionId(String);
31
32impl std::convert::AsRef<str> for CollectionId {
33    fn as_ref(&self) -> &str {
34        self.0.as_ref()
35    }
36}
37
38impl std::convert::TryFrom<&str> for CollectionId {
39    type Error = Error;
40
41    fn try_from(s: &str) -> Result<Self, Self::Error> {
42        Self::try_from(s.to_string())
43    }
44}
45
46impl std::convert::TryFrom<String> for CollectionId {
47    type Error = Error;
48
49    fn try_from(s: String) -> Result<Self, Self::Error> {
50        // <https://firebase.google.com/docs/firestore/quotas#collections_documents_and_fields>
51        if !(1..=1500).contains(&s.len()) {
52            return Err(Error::from(ErrorKind::LengthOutOfBounds));
53        }
54        if s.contains('/') {
55            return Err(Error::from(ErrorKind::ContainsSlash));
56        }
57        if s == "." || s == ".." {
58            return Err(Error::from(ErrorKind::SinglePeriodOrDoublePeriods));
59        }
60        if s.starts_with("__") && s.ends_with("__") {
61            return Err(Error::from(ErrorKind::MatchesReservedIdPattern));
62        }
63        Ok(Self(s))
64    }
65}
66
67impl std::fmt::Display for CollectionId {
68    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
69        self.0.fmt(f)
70    }
71}
72
73impl std::str::FromStr for CollectionId {
74    type Err = Error;
75
76    fn from_str(s: &str) -> Result<Self, Self::Err> {
77        Self::try_from(s)
78    }
79}
80
81#[cfg(test)]
82mod tests {
83    use std::str::FromStr;
84
85    use super::*;
86
87    #[test]
88    fn test() -> anyhow::Result<()> {
89        let s = "chatrooms";
90        let collection_id = CollectionId::from_str(s)?;
91        assert_eq!(collection_id.to_string(), s);
92
93        let s = "messages";
94        let collection_id = CollectionId::from_str(s)?;
95        assert_eq!(collection_id.to_string(), s);
96
97        assert_eq!(collection_id.as_ref(), s);
98        Ok(())
99    }
100
101    #[test]
102    fn test_impl_from_str_and_impl_try_from_string() -> anyhow::Result<()> {
103        for (s, expected) in [
104            ("", false),
105            ("x".repeat(1501).as_ref(), false),
106            ("x".repeat(1500).as_ref(), true),
107            ("chat/rooms", false),
108            (".", false),
109            (".x", true),
110            ("..", false),
111            ("..x", true),
112            ("__x__", false),
113            ("__x", true),
114            ("x__", true),
115        ] {
116            assert_eq!(CollectionId::from_str(s).is_ok(), expected);
117            assert_eq!(CollectionId::try_from(s).is_ok(), expected);
118            assert_eq!(CollectionId::try_from(s.to_string()).is_ok(), expected);
119            if expected {
120                assert_eq!(CollectionId::from_str(s)?, CollectionId::try_from(s)?);
121                assert_eq!(
122                    CollectionId::from_str(s)?,
123                    CollectionId::try_from(s.to_string())?
124                );
125                assert_eq!(CollectionId::from_str(s)?.to_string(), s);
126            }
127        }
128        Ok(())
129    }
130}