firestore_path/
root_document_name.rs

1use std::str::FromStr;
2
3use crate::{
4    error::ErrorKind, CollectionName, CollectionPath, DatabaseId, DatabaseName, DocumentName,
5    DocumentPath, Error, ProjectId,
6};
7
8/// A root document name.
9///
10/// # Format
11///
12/// `{database_name}/documents`
13///
14/// # Examples
15///
16/// ```rust
17/// # fn main() -> anyhow::Result<()> {
18/// use firestore_path::RootDocumentName;
19/// use std::str::FromStr;
20///
21/// let root_document_name = RootDocumentName::from_str(
22///     "projects/my-project/databases/my-database/documents"
23/// )?;
24/// assert_eq!(
25///     root_document_name.to_string(),
26///     "projects/my-project/databases/my-database/documents"
27/// );
28/// #     Ok(())
29/// # }
30/// ```
31///
32#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
33pub struct RootDocumentName {
34    database_name: DatabaseName,
35}
36
37impl RootDocumentName {
38    /// Creates a new `RootDocumentName`.
39    ///
40    /// # Examples
41    ///
42    /// ```rust
43    /// # fn main() -> anyhow::Result<()> {
44    /// use firestore_path::{DatabaseName,RootDocumentName};
45    /// use std::str::FromStr;
46    ///
47    /// let database_name = DatabaseName::from_str(
48    ///     "projects/my-project/databases/my-database"
49    /// )?;
50    /// assert_eq!(
51    ///     RootDocumentName::new(database_name),
52    ///     RootDocumentName::from_str(
53    ///         "projects/my-project/databases/my-database/documents"
54    ///     )?
55    /// );
56    /// #     Ok(())
57    /// # }
58    /// ```
59    pub fn new(database_name: DatabaseName) -> Self {
60        Self { database_name }
61    }
62
63    /// Creates a new `CollectionName` from this `RootDocumentName` and `collection_path`.
64    ///
65    /// # Examples
66    ///
67    /// ```rust
68    /// # fn main() -> anyhow::Result<()> {
69    /// use firestore_path::{CollectionId,CollectionName,CollectionPath,RootDocumentName};
70    /// use std::str::FromStr;
71    ///
72    /// let root_document_name = RootDocumentName::from_str(
73    ///     "projects/my-project/databases/my-database/documents"
74    /// )?;
75    /// assert_eq!(
76    ///     root_document_name.collection("chatrooms")?,
77    ///     CollectionName::from_str(
78    ///         "projects/my-project/databases/my-database/documents/chatrooms"
79    ///     )?
80    /// );
81    /// assert_eq!(
82    ///     root_document_name.collection("chatrooms/chatroom1/messages")?,
83    ///     CollectionName::from_str(
84    ///         "projects/my-project/databases/my-database/documents/chatrooms/chatroom1/messages"
85    ///     )?
86    /// );
87    /// assert_eq!(
88    ///     root_document_name.collection(CollectionId::from_str("chatrooms")?)?,
89    ///     CollectionName::from_str(
90    ///         "projects/my-project/databases/my-database/documents/chatrooms"
91    ///     )?
92    /// );
93    /// assert_eq!(
94    ///     root_document_name.collection(CollectionPath::from_str("chatrooms/chatroom1/messages")?)?,
95    ///     CollectionName::from_str(
96    ///         "projects/my-project/databases/my-database/documents/chatrooms/chatroom1/messages"
97    ///     )?
98    /// );
99    ///
100    /// #     Ok(())
101    /// # }
102    /// ```
103    ///
104    pub fn collection<E, T>(&self, collection_path: T) -> Result<CollectionName, Error>
105    where
106        E: std::fmt::Display,
107        T: TryInto<CollectionPath, Error = E>,
108    {
109        self.clone().into_collection(collection_path)
110    }
111
112    /// Creates a new `CollectionName` by consuming `RootDocumentName` with the provided `collection_path`.
113    ///
114    /// # Examples
115    ///
116    /// ```rust
117    /// # fn main() -> anyhow::Result<()> {
118    /// use firestore_path::{CollectionId,CollectionName,CollectionPath,RootDocumentName};
119    /// use std::str::FromStr;
120    ///
121    /// let root_document_name = RootDocumentName::from_str(
122    ///     "projects/my-project/databases/my-database/documents"
123    /// )?;
124    /// assert_eq!(
125    ///     root_document_name.clone().into_collection("chatrooms")?,
126    ///     CollectionName::from_str(
127    ///         "projects/my-project/databases/my-database/documents/chatrooms"
128    ///     )?
129    /// );
130    /// assert_eq!(
131    ///     root_document_name.clone().into_collection("chatrooms/chatroom1/messages")?,
132    ///     CollectionName::from_str(
133    ///         "projects/my-project/databases/my-database/documents/chatrooms/chatroom1/messages"
134    ///     )?
135    /// );
136    /// assert_eq!(
137    ///     root_document_name.clone().into_collection(CollectionId::from_str("chatrooms")?)?,
138    ///     CollectionName::from_str(
139    ///         "projects/my-project/databases/my-database/documents/chatrooms"
140    ///     )?
141    /// );
142    /// assert_eq!(
143    ///     root_document_name.into_collection(CollectionPath::from_str("chatrooms/chatroom1/messages")?)?,
144    ///     CollectionName::from_str(
145    ///         "projects/my-project/databases/my-database/documents/chatrooms/chatroom1/messages"
146    ///     )?
147    /// );
148    ///
149    /// #     Ok(())
150    /// # }
151    /// ```
152    ///
153    pub fn into_collection<E, T>(self, collection_path: T) -> Result<CollectionName, Error>
154    where
155        E: std::fmt::Display,
156        T: TryInto<CollectionPath, Error = E>,
157    {
158        let collection_path = collection_path
159            .try_into()
160            .map_err(|e| Error::from(ErrorKind::CollectionPathConversion(e.to_string())))?;
161        Ok(CollectionName::new(self, collection_path))
162    }
163
164    /// Creates a new `DocumentName` from this `RootDocumentName` and `document_path`.
165    ///
166    /// # Examples
167    ///
168    /// ```rust
169    /// # fn main() -> anyhow::Result<()> {
170    /// use firestore_path::{DocumentName,DocumentPath,RootDocumentName};
171    /// use std::str::FromStr;
172    ///
173    /// let root_document_name = RootDocumentName::from_str(
174    ///     "projects/my-project/databases/my-database/documents"
175    /// )?;
176    /// assert_eq!(
177    ///     root_document_name.doc("chatrooms/chatroom1")?,
178    ///     DocumentName::from_str(
179    ///         "projects/my-project/databases/my-database/documents/chatrooms/chatroom1"
180    ///     )?
181    /// );
182    /// assert_eq!(
183    ///     root_document_name.doc("chatrooms/chatroom1/messages/message1")?,
184    ///     DocumentName::from_str(
185    ///         "projects/my-project/databases/my-database/documents/chatrooms/chatroom1/messages/message1"
186    ///     )?
187    /// );
188    /// assert_eq!(
189    ///     root_document_name.doc(DocumentPath::from_str("chatrooms/chatroom1")?)?,
190    ///     DocumentName::from_str(
191    ///         "projects/my-project/databases/my-database/documents/chatrooms/chatroom1"
192    ///     )?
193    /// );
194    /// assert_eq!(
195    ///     root_document_name.doc(DocumentPath::from_str("chatrooms/chatroom1/messages/message1")?)?,
196    ///     DocumentName::from_str(
197    ///         "projects/my-project/databases/my-database/documents/chatrooms/chatroom1/messages/message1"
198    ///     )?
199    /// );
200    ///
201    /// #     Ok(())
202    /// # }
203    /// ```
204    ///
205    pub fn doc<E, T>(&self, document_path: T) -> Result<DocumentName, Error>
206    where
207        E: std::fmt::Display,
208        T: TryInto<DocumentPath, Error = E>,
209    {
210        self.clone().into_doc(document_path)
211    }
212
213    /// Creates a new `DocumentName` by consuming the `RootDocumentName` with the provided `document_path`.
214    ///
215    /// # Examples
216    ///
217    /// ```rust
218    /// # fn main() -> anyhow::Result<()> {
219    /// use firestore_path::{DocumentName,DocumentPath,RootDocumentName};
220    /// use std::str::FromStr;
221    ///
222    /// let root_document_name = RootDocumentName::from_str(
223    ///     "projects/my-project/databases/my-database/documents"
224    /// )?;
225    /// assert_eq!(
226    ///     root_document_name.clone().into_doc("chatrooms/chatroom1")?,
227    ///     DocumentName::from_str(
228    ///         "projects/my-project/databases/my-database/documents/chatrooms/chatroom1"
229    ///     )?
230    /// );
231    /// assert_eq!(
232    ///     root_document_name.clone().into_doc("chatrooms/chatroom1/messages/message1")?,
233    ///     DocumentName::from_str(
234    ///         "projects/my-project/databases/my-database/documents/chatrooms/chatroom1/messages/message1"
235    ///     )?
236    /// );
237    /// assert_eq!(
238    ///     root_document_name.clone().into_doc(DocumentPath::from_str("chatrooms/chatroom1")?)?,
239    ///     DocumentName::from_str(
240    ///         "projects/my-project/databases/my-database/documents/chatrooms/chatroom1"
241    ///     )?
242    /// );
243    /// assert_eq!(
244    ///     root_document_name.doc(DocumentPath::from_str("chatrooms/chatroom1/messages/message1")?)?,
245    ///     DocumentName::from_str(
246    ///         "projects/my-project/databases/my-database/documents/chatrooms/chatroom1/messages/message1"
247    ///     )?
248    /// );
249    ///
250    /// #     Ok(())
251    /// # }
252    /// ```
253    ///
254    pub fn into_doc<E, T>(self, document_path: T) -> Result<DocumentName, Error>
255    where
256        E: std::fmt::Display,
257        T: TryInto<DocumentPath, Error = E>,
258    {
259        let document_path = document_path
260            .try_into()
261            .map_err(|e| Error::from(ErrorKind::DocumentPathConversion(e.to_string())))?;
262        Ok(DocumentName::new(self, document_path))
263    }
264
265    pub(crate) fn as_database_name(&self) -> &DatabaseName {
266        &self.database_name
267    }
268}
269
270impl std::convert::From<DatabaseName> for RootDocumentName {
271    fn from(database_name: DatabaseName) -> Self {
272        Self { database_name }
273    }
274}
275
276impl std::convert::From<RootDocumentName> for DatabaseName {
277    fn from(root_document_name: RootDocumentName) -> Self {
278        root_document_name.database_name
279    }
280}
281
282impl std::convert::TryFrom<&str> for RootDocumentName {
283    type Error = Error;
284
285    fn try_from(s: &str) -> Result<Self, Self::Error> {
286        if !(1..=1_024 * 6).contains(&s.len()) {
287            return Err(Error::from(ErrorKind::LengthOutOfBounds));
288        }
289
290        let parts = s.split('/').collect::<Vec<&str>>();
291        if parts.len() != 5 {
292            return Err(Error::from(ErrorKind::InvalidNumberOfPathComponents));
293        }
294        if parts[0] != "projects" || parts[2] != "databases" || parts[4] != "documents" {
295            return Err(Error::from(ErrorKind::InvalidName));
296        }
297
298        let project_id = ProjectId::from_str(parts[1])?;
299        let database_id = DatabaseId::from_str(parts[3])?;
300        let database_name = DatabaseName::new(project_id, database_id);
301        Ok(Self { database_name })
302    }
303}
304
305impl std::convert::TryFrom<String> for RootDocumentName {
306    type Error = Error;
307
308    fn try_from(s: String) -> Result<Self, Self::Error> {
309        Self::try_from(s.as_str())
310    }
311}
312
313impl std::fmt::Display for RootDocumentName {
314    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
315        write!(f, "{}/documents", self.database_name)
316    }
317}
318
319impl std::str::FromStr for RootDocumentName {
320    type Err = Error;
321
322    fn from_str(s: &str) -> Result<Self, Self::Err> {
323        Self::try_from(s)
324    }
325}
326
327#[cfg(test)]
328mod tests {
329    use std::str::FromStr;
330
331    use super::*;
332
333    #[test]
334    fn test() -> anyhow::Result<()> {
335        let s = "projects/my-project/databases/my-database/documents";
336        let root_document_name = RootDocumentName::from_str(s)?;
337        assert_eq!(root_document_name.to_string(), s);
338        Ok(())
339    }
340
341    #[test]
342    fn test_impl_from_str_and_impl_try_from_string() -> anyhow::Result<()> {
343        for (s, expected) in [
344            ("", false),
345            ("projects/my-project/databases/my-database/documents", true),
346            ("x".repeat(1024 * 6 + 1).as_ref(), false),
347            ("p/my-project/databases/my-database/documents", false),
348            ("projects/my-project/d/my-database/documents", false),
349            ("projects/my-project/databases/my-database/d", false),
350            ("projects/P/databases/my-database/d", false),
351            ("projects/my-project/databases/D/d", false),
352        ] {
353            assert_eq!(RootDocumentName::from_str(s).is_ok(), expected);
354            assert_eq!(RootDocumentName::try_from(s).is_ok(), expected);
355            assert_eq!(RootDocumentName::try_from(s.to_string()).is_ok(), expected);
356            if expected {
357                assert_eq!(
358                    RootDocumentName::from_str(s)?,
359                    RootDocumentName::try_from(s)?
360                );
361                assert_eq!(
362                    RootDocumentName::from_str(s)?,
363                    RootDocumentName::try_from(s.to_string())?
364                );
365                assert_eq!(RootDocumentName::from_str(s)?.to_string(), s);
366            }
367        }
368        Ok(())
369    }
370}