firestore_path/
document_id.rs

1use crate::{error::ErrorKind, Error};
2
3/// A document 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/// > - If you import Datastore entities into a Firestore database, numeric entity IDs are exposed as __id[0-9]+__
15///
16/// # Examples
17///
18/// ```rust
19/// # fn main() -> anyhow::Result<()> {
20/// use firestore_path::DocumentId;
21/// use std::str::FromStr;
22///
23/// let document_id = DocumentId::from_str("chatroom1")?;
24/// assert_eq!(document_id.as_ref(), "chatroom1");
25/// assert_eq!(document_id.to_string(), "chatroom1");
26/// #     Ok(())
27/// # }
28/// ```
29///
30#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
31pub struct DocumentId(String);
32
33impl std::convert::AsRef<str> for DocumentId {
34    fn as_ref(&self) -> &str {
35        self.0.as_ref()
36    }
37}
38
39impl std::convert::TryFrom<&str> for DocumentId {
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 DocumentId {
48    type Error = Error;
49
50    fn try_from(s: String) -> Result<Self, Self::Error> {
51        // <https://firebase.google.com/docs/firestore/quotas#collections_documents_and_fields>
52        if !(1..=1500).contains(&s.len()) {
53            return Err(Error::from(ErrorKind::LengthOutOfBounds));
54        }
55
56        if s.contains('/') {
57            return Err(Error::from(ErrorKind::ContainsSlash));
58        }
59
60        if s == "." || s == ".." {
61            return Err(Error::from(ErrorKind::SinglePeriodOrDoublePeriods));
62        }
63
64        if s.starts_with("__") && s.ends_with("__") {
65            return Err(Error::from(ErrorKind::MatchesReservedIdPattern));
66        }
67
68        // TODO: Datastore entities
69
70        Ok(Self(s))
71    }
72}
73
74impl std::fmt::Display for DocumentId {
75    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
76        self.0.fmt(f)
77    }
78}
79
80impl std::str::FromStr for DocumentId {
81    type Err = Error;
82
83    fn from_str(s: &str) -> Result<Self, Self::Err> {
84        Self::try_from(s)
85    }
86}
87
88#[cfg(test)]
89mod tests {
90    use std::str::FromStr;
91
92    use super::*;
93
94    #[test]
95    fn test() -> anyhow::Result<()> {
96        let s = "chatroom1";
97        let document_id = DocumentId::from_str(s)?;
98        assert_eq!(document_id.to_string(), s);
99
100        assert_eq!(document_id.as_ref(), s);
101        Ok(())
102    }
103
104    #[test]
105    fn test_impl_from_str_and_impl_try_from_string() -> anyhow::Result<()> {
106        for (s, expected) in [
107            ("chatroom1", true),
108            ("", false),
109            ("x", true),
110            ("x".repeat(1501).as_ref(), false),
111            ("x".repeat(1500).as_ref(), true),
112            ("chat/room1", false),
113            (".", false),
114            (".x", true),
115            ("..", false),
116            ("..x", true),
117            ("__x__", false),
118            ("__x", true),
119            ("x__", true),
120        ] {
121            assert_eq!(DocumentId::from_str(s).is_ok(), expected);
122            assert_eq!(DocumentId::try_from(s.to_string()).is_ok(), expected);
123            if expected {
124                assert_eq!(
125                    DocumentId::from_str(s)?,
126                    DocumentId::try_from(s.to_string())?
127                );
128                assert_eq!(DocumentId::from_str(s)?.to_string(), s);
129            }
130        }
131        Ok(())
132    }
133}