firestore_path/
collection_name.rs

1use std::str::FromStr;
2
3use crate::{
4    error::ErrorKind, CollectionId, CollectionPath, DatabaseName, DocumentId, DocumentName,
5    DocumentPath, Error, RootDocumentName,
6};
7
8/// A collection name.
9///
10/// # Format
11///
12/// `{root_document_name}/{collection_path}`
13///
14/// # Examples
15///
16/// ```rust
17/// # fn main() -> anyhow::Result<()> {
18/// use firestore_path::{CollectionName,CollectionPath};
19/// use std::str::FromStr;
20///
21/// let collection_name = CollectionName::from_str(
22///     "projects/my-project/databases/my-database/documents/chatrooms"
23/// )?;
24/// assert_eq!(
25///     collection_name.to_string(),
26///     "projects/my-project/databases/my-database/documents/chatrooms"
27/// );
28///
29/// assert_eq!(
30///     collection_name.collection_path(),
31///     &CollectionPath::from_str("chatrooms")?
32/// );
33///
34/// assert_eq!(
35///     CollectionPath::from(collection_name),
36///     CollectionPath::from_str("chatrooms")?
37/// );
38///
39/// #     Ok(())
40/// # }
41/// ```
42#[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
43pub struct CollectionName {
44    collection_path: CollectionPath,
45    root_document_name: RootDocumentName,
46}
47
48impl CollectionName {
49    /// Creates a new `CollectionName`.
50    ///
51    /// # Examples
52    ///
53    /// ```rust
54    /// # fn main() -> anyhow::Result<()> {
55    /// use firestore_path::{CollectionName,CollectionPath,DatabaseName,RootDocumentName};
56    /// use std::str::FromStr;
57    ///
58    /// let root_document_name = RootDocumentName::from_str("projects/my-project/databases/my-database/documents")?;
59    /// let collection_path = CollectionPath::from_str("chatrooms")?;
60    /// let collection_name = CollectionName::new(root_document_name, collection_path);
61    /// assert_eq!(
62    ///     collection_name.to_string(),
63    ///     "projects/my-project/databases/my-database/documents/chatrooms"
64    /// );
65    ///
66    /// let database_name = DatabaseName::from_str("projects/my-project/databases/my-database")?;
67    /// let collection_path = CollectionPath::from_str("chatrooms")?;
68    /// let collection_name = CollectionName::new(database_name, collection_path);
69    /// assert_eq!(
70    ///     collection_name.to_string(),
71    ///     "projects/my-project/databases/my-database/documents/chatrooms"
72    /// );
73    /// #     Ok(())
74    /// # }
75    /// ```
76    pub fn new<D>(root_document_name: D, collection_path: CollectionPath) -> Self
77    where
78        D: Into<RootDocumentName>,
79    {
80        Self {
81            collection_path,
82            root_document_name: root_document_name.into(),
83        }
84    }
85
86    /// Returns the `CollectionId` of this `CollectionName`.
87    ///
88    /// # Examples
89    ///
90    /// ```rust
91    /// # fn main() -> anyhow::Result<()> {
92    /// use firestore_path::{CollectionId,CollectionName};
93    /// use std::str::FromStr;
94    ///
95    /// let collection_name = CollectionName::from_str(
96    ///     "projects/my-project/databases/my-database/documents/chatrooms"
97    /// )?;
98    /// assert_eq!(
99    ///     collection_name.collection_id(),
100    ///     &CollectionId::from_str("chatrooms")?
101    /// );
102    /// #     Ok(())
103    /// # }
104    /// ```
105    pub fn collection_id(&self) -> &CollectionId {
106        self.collection_path.collection_id()
107    }
108
109    /// Returns the `CollectionPath` of this `CollectionName`.
110    ///
111    /// # Examples
112    ///
113    /// ```rust
114    /// # fn main() -> anyhow::Result<()> {
115    /// use firestore_path::{CollectionName,CollectionPath};
116    /// use std::str::FromStr;
117    ///
118    /// let collection_name = CollectionName::from_str(
119    ///     "projects/my-project/databases/my-database/documents/chatrooms"
120    /// )?;
121    /// assert_eq!(
122    ///     collection_name.collection_path(),
123    ///     &CollectionPath::from_str("chatrooms")?
124    /// );
125    /// #     Ok(())
126    /// # }
127    /// ```
128    pub fn collection_path(&self) -> &CollectionPath {
129        &self.collection_path
130    }
131
132    /// Returns the `DatabaseName` of this `CollectionName`.
133    ///
134    /// # Examples
135    ///
136    /// ```rust
137    /// # fn main() -> anyhow::Result<()> {
138    /// use firestore_path::{DatabaseName,CollectionName};
139    /// use std::str::FromStr;
140    ///
141    /// let collection_name = CollectionName::from_str(
142    ///     "projects/my-project/databases/my-database/documents/chatrooms"
143    /// )?;
144    /// assert_eq!(
145    ///     collection_name.database_name(),
146    ///     &DatabaseName::from_str("projects/my-project/databases/my-database")?
147    /// );
148    /// #     Ok(())
149    /// # }
150    /// ```
151    ///
152    pub fn database_name(&self) -> &DatabaseName {
153        self.root_document_name.as_database_name()
154    }
155
156    /// Creates a new `DocumentName` from this `CollectionName` and `document_id`.
157    ///
158    /// # Examples
159    ///
160    /// ```rust
161    /// # fn main() -> anyhow::Result<()> {
162    /// use firestore_path::{CollectionId,CollectionName,DocumentName};
163    /// use std::str::FromStr;
164    ///
165    /// let collection_name = CollectionName::from_str(
166    ///     "projects/my-project/databases/my-database/documents/chatrooms"
167    /// )?;
168    /// assert_eq!(
169    ///     collection_name.doc("chatroom1")?,
170    ///     DocumentName::from_str(
171    ///         "projects/my-project/databases/my-database/documents/chatrooms/chatroom1"
172    ///     )?
173    /// );
174    /// assert_eq!(
175    ///     collection_name.doc("chatroom2")?,
176    ///     DocumentName::from_str(
177    ///         "projects/my-project/databases/my-database/documents/chatrooms/chatroom2"
178    ///     )?
179    /// );
180    /// #     Ok(())
181    /// # }
182    /// ```
183    pub fn doc<E, T>(&self, document_id: T) -> Result<DocumentName, Error>
184    where
185        E: std::fmt::Display,
186        T: TryInto<DocumentId, Error = E>,
187    {
188        self.clone().into_doc(document_id)
189    }
190
191    /// Creates a new `DocumentName` by consuming the `CollectionName` with the provided `document_id`.
192    ///
193    /// # Examples
194    ///
195    /// ```rust
196    /// # fn main() -> anyhow::Result<()> {
197    /// use firestore_path::{CollectionId,CollectionName,DocumentName};
198    /// use std::str::FromStr;
199    ///
200    /// let collection_name = CollectionName::from_str(
201    ///     "projects/my-project/databases/my-database/documents/chatrooms"
202    /// )?;
203    /// assert_eq!(
204    ///     collection_name.clone().into_doc("chatroom1")?,
205    ///     DocumentName::from_str(
206    ///         "projects/my-project/databases/my-database/documents/chatrooms/chatroom1"
207    ///     )?
208    /// );
209    /// assert_eq!(
210    ///     collection_name.into_doc("chatroom2")?,
211    ///     DocumentName::from_str(
212    ///         "projects/my-project/databases/my-database/documents/chatrooms/chatroom2"
213    ///     )?
214    /// );
215    /// #     Ok(())
216    /// # }
217    /// ```
218    pub fn into_doc<E, T>(self, document_id: T) -> Result<DocumentName, Error>
219    where
220        E: std::fmt::Display,
221        T: TryInto<DocumentId, Error = E>,
222    {
223        let document_id = document_id
224            .try_into()
225            .map_err(|e| Error::from(ErrorKind::DocumentIdConversion(e.to_string())))?;
226        let document_path = DocumentPath::new(self.collection_path, document_id);
227        let document_name = DocumentName::new(self.root_document_name, document_path);
228        Ok(document_name)
229    }
230
231    /// Consumes the `CollectionName`, returning the parent `DocumentName`.
232    ///
233    /// # Examples
234    ///
235    /// ```rust
236    /// # fn main() -> anyhow::Result<()> {
237    /// use firestore_path::{CollectionId,CollectionName,DocumentName};
238    /// use std::str::FromStr;
239    ///
240    /// let collection_name = CollectionName::from_str(
241    ///     "projects/my-project/databases/my-database/documents/chatrooms"
242    /// )?;
243    /// assert_eq!(collection_name.into_parent(), None);
244    ///
245    /// let collection_name = CollectionName::from_str(
246    ///     "projects/my-project/databases/my-database/documents/chatrooms/chatroom1/messages"
247    /// )?;
248    /// assert_eq!(
249    ///     collection_name.clone().into_parent(),
250    ///     Some(DocumentName::from_str(
251    ///       "projects/my-project/databases/my-database/documents/chatrooms/chatroom1"
252    ///     )?)
253    /// );
254    /// assert_eq!(
255    ///     collection_name.into_parent(),
256    ///     Some(DocumentName::from_str(
257    ///       "projects/my-project/databases/my-database/documents/chatrooms/chatroom1"
258    ///     )?)
259    /// );
260    /// #     Ok(())
261    /// # }
262    /// ```
263    pub fn into_parent(self) -> Option<DocumentName> {
264        Option::<DocumentPath>::from(self.collection_path).map(|document_path| {
265            DocumentName::new(DatabaseName::from(self.root_document_name), document_path)
266        })
267    }
268
269    /// Consumes the `CollectionName`, returning the `RootDocumentName`.
270    ///
271    /// # Examples
272    ///
273    /// ```rust
274    /// # fn main() -> anyhow::Result<()> {
275    /// use firestore_path::{CollectionName,RootDocumentName};
276    /// use std::str::FromStr;
277    ///
278    /// let collection_name = CollectionName::from_str(
279    ///     "projects/my-project/databases/my-database/documents/chatrooms"
280    /// )?;
281    /// let root_document_name = collection_name.into_root_document_name();
282    /// assert_eq!(
283    ///     root_document_name,
284    ///     RootDocumentName::from_str(
285    ///         "projects/my-project/databases/my-database/documents"
286    ///     )?
287    /// );
288    /// #     Ok(())
289    /// # }
290    /// ```
291    pub fn into_root_document_name(self) -> RootDocumentName {
292        self.root_document_name
293    }
294
295    /// Returns the parent `DocumentName` of this `CollectionName`.
296    ///
297    /// # Examples
298    ///
299    /// ```rust
300    /// # fn main() -> anyhow::Result<()> {
301    /// use firestore_path::{CollectionId,CollectionName,DocumentName};
302    /// use std::str::FromStr;
303    ///
304    /// let collection_name = CollectionName::from_str(
305    ///     "projects/my-project/databases/my-database/documents/chatrooms"
306    /// )?;
307    /// assert_eq!(collection_name.parent(), None);
308    ///
309    /// let collection_name = CollectionName::from_str(
310    ///     "projects/my-project/databases/my-database/documents/chatrooms/chatroom1/messages"
311    /// )?;
312    /// assert_eq!(
313    ///     collection_name.parent(),
314    ///     Some(DocumentName::from_str(
315    ///       "projects/my-project/databases/my-database/documents/chatrooms/chatroom1"
316    ///     )?)
317    /// );
318    /// assert_eq!(
319    ///     collection_name.parent(),
320    ///     Some(DocumentName::from_str(
321    ///       "projects/my-project/databases/my-database/documents/chatrooms/chatroom1"
322    ///     )?)
323    /// );
324    /// #     Ok(())
325    /// # }
326    /// ```
327    pub fn parent(&self) -> Option<DocumentName> {
328        self.clone().into_parent()
329    }
330
331    /// Returns the `RootDocumentName` of this `CollectionName`.
332    ///
333    /// # Examples
334    ///
335    /// ```rust
336    /// # fn main() -> anyhow::Result<()> {
337    /// use firestore_path::{CollectionName,RootDocumentName};
338    /// use std::str::FromStr;
339    ///
340    /// let collection_name = CollectionName::from_str(
341    ///     "projects/my-project/databases/my-database/documents/chatrooms"
342    /// )?;
343    /// let root_document_name = collection_name.root_document_name();
344    /// assert_eq!(
345    ///     root_document_name,
346    ///     &RootDocumentName::from_str(
347    ///         "projects/my-project/databases/my-database/documents"
348    ///     )?
349    /// );
350    /// #     Ok(())
351    /// # }
352    /// ```
353    pub fn root_document_name(&self) -> &RootDocumentName {
354        &self.root_document_name
355    }
356}
357
358impl std::convert::From<CollectionName> for CollectionId {
359    fn from(collection_name: CollectionName) -> Self {
360        Self::from(collection_name.collection_path)
361    }
362}
363
364impl std::convert::From<CollectionName> for CollectionPath {
365    fn from(collection_name: CollectionName) -> Self {
366        collection_name.collection_path
367    }
368}
369
370impl std::convert::From<CollectionName> for DatabaseName {
371    fn from(collection_name: CollectionName) -> Self {
372        Self::from(collection_name.root_document_name)
373    }
374}
375
376impl std::convert::TryFrom<&str> for CollectionName {
377    type Error = Error;
378
379    fn try_from(s: &str) -> Result<Self, Self::Error> {
380        // <https://firebase.google.com/docs/firestore/quotas#collections_documents_and_fields>
381        if !(1..=6_144).contains(&s.len()) {
382            return Err(Error::from(ErrorKind::LengthOutOfBounds));
383        }
384
385        let parts = s.split('/').collect::<Vec<&str>>();
386        if parts.len() < 5 + 1 || (parts.len() - 5) % 2 == 0 {
387            return Err(Error::from(ErrorKind::InvalidNumberOfPathComponents));
388        }
389
390        Ok(Self {
391            collection_path: CollectionPath::from_str(&parts[5..].join("/"))?,
392            root_document_name: RootDocumentName::from_str(&parts[0..5].join("/"))?,
393        })
394    }
395}
396
397impl std::convert::TryFrom<String> for CollectionName {
398    type Error = Error;
399
400    fn try_from(s: String) -> Result<Self, Self::Error> {
401        Self::try_from(s.as_str())
402    }
403}
404
405impl std::fmt::Display for CollectionName {
406    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
407        write!(f, "{}/{}", self.root_document_name, self.collection_path)
408    }
409}
410
411impl std::str::FromStr for CollectionName {
412    type Err = Error;
413
414    fn from_str(s: &str) -> Result<Self, Self::Err> {
415        Self::try_from(s)
416    }
417}
418
419#[cfg(test)]
420mod tests {
421    use std::str::FromStr;
422
423    use crate::CollectionId;
424
425    use super::*;
426
427    #[test]
428    fn test() -> anyhow::Result<()> {
429        let s = "projects/my-project/databases/my-database/documents/chatrooms";
430        let collection_name = CollectionName::from_str(s)?;
431        assert_eq!(collection_name.to_string(), s);
432
433        let s = "projects/my-project/databases/my-database/documents/chatrooms/chatroom1/messages";
434        let collection_name = CollectionName::from_str(s)?;
435        assert_eq!(collection_name.to_string(), s);
436        Ok(())
437    }
438
439    #[test]
440    fn test_collection_id() -> anyhow::Result<()> {
441        let s = "projects/my-project/databases/my-database/documents/chatrooms";
442        let collection_name = CollectionName::from_str(s)?;
443        assert_eq!(
444            collection_name.collection_id(),
445            &CollectionId::from_str("chatrooms")?
446        );
447        Ok(())
448    }
449
450    #[test]
451    fn test_doc() -> anyhow::Result<()> {
452        let collection_name = CollectionName::from_str(
453            "projects/my-project/databases/my-database/documents/chatrooms",
454        )?;
455        let document_name = collection_name.doc("chatroom1")?;
456        assert_eq!(
457            document_name,
458            DocumentName::from_str(
459                "projects/my-project/databases/my-database/documents/chatrooms/chatroom1"
460            )?
461        );
462        let document_name = collection_name.doc("chatroom2")?;
463        assert_eq!(
464            document_name,
465            DocumentName::from_str(
466                "projects/my-project/databases/my-database/documents/chatrooms/chatroom2"
467            )?
468        );
469
470        let collection_name = CollectionName::from_str(
471            "projects/my-project/databases/my-database/documents/chatrooms/chatroom1/messages",
472        )?;
473        let document_name = collection_name.doc("message1")?;
474        assert_eq!(
475            document_name,
476            DocumentName::from_str(
477                "projects/my-project/databases/my-database/documents/chatrooms/chatroom1/messages/message1"
478            )?
479        );
480
481        let collection_name = CollectionName::from_str(
482            "projects/my-project/databases/my-database/documents/chatrooms",
483        )?;
484        let document_id = DocumentId::from_str("chatroom1")?;
485        let document_name = collection_name.doc(document_id)?;
486        assert_eq!(
487            document_name,
488            DocumentName::from_str(
489                "projects/my-project/databases/my-database/documents/chatrooms/chatroom1"
490            )?
491        );
492
493        Ok(())
494    }
495
496    #[test]
497    fn test_impl_from_collection_name_for_collection_id() -> anyhow::Result<()> {
498        let s = "projects/my-project/databases/my-database/documents/chatrooms";
499        let collection_name = CollectionName::from_str(s)?;
500        assert_eq!(
501            CollectionId::from(collection_name),
502            CollectionId::from_str("chatrooms")?
503        );
504        Ok(())
505    }
506
507    #[test]
508    fn test_impl_from_collection_name_for_database_name() -> anyhow::Result<()> {
509        let s = "projects/my-project/databases/my-database/documents/chatrooms";
510        let collection_name = CollectionName::from_str(s)?;
511        assert_eq!(
512            DatabaseName::from(collection_name),
513            DatabaseName::from_str("projects/my-project/databases/my-database")?
514        );
515        Ok(())
516    }
517
518    #[test]
519    fn test_impl_from_str_and_impl_try_from_string() -> anyhow::Result<()> {
520        let b = "projects/my-project/databases/my-database/documents";
521        let c1 = "x".repeat(1500);
522        let d1 = "x".repeat(1500);
523        let c2 = "y".repeat(1500);
524        let d2 = "y".repeat(1500);
525        let c3_ok = "z".repeat(88);
526        let c3_err = "z".repeat(88 + 1);
527        let s1 = format!("{}/{}/{}/{}/{}/{}", b, c1, d1, c2, d2, c3_ok);
528        assert_eq!(s1.len(), 6_144);
529        let s2 = format!("{}/{}/{}/{}/{}/{}", b, c1, d1, c2, d2, c3_err);
530        assert_eq!(s2.len(), 6_145);
531        for (s, expected) in [
532            ("", false),
533            ("projects/my-project/databases/my-database/documents", false),
534            (
535                "projects/my-project/databases/my-database/documents/c",
536                true,
537            ),
538            (
539                "projects/my-project/databases/my-database/documents/c/d",
540                false,
541            ),
542            (
543                "projects/my-project/databases/my-database/documents/c/d/c",
544                true,
545            ),
546            (s1.as_str(), true),
547            (s2.as_str(), false),
548        ] {
549            assert_eq!(CollectionName::from_str(s).is_ok(), expected);
550            assert_eq!(CollectionName::try_from(s).is_ok(), expected);
551            assert_eq!(CollectionName::try_from(s.to_string()).is_ok(), expected);
552            if expected {
553                assert_eq!(CollectionName::from_str(s)?, CollectionName::try_from(s)?);
554                assert_eq!(
555                    CollectionName::from_str(s)?,
556                    CollectionName::try_from(s.to_string())?
557                );
558                assert_eq!(CollectionName::from_str(s)?.to_string(), s);
559            }
560        }
561        Ok(())
562    }
563
564    #[test]
565    fn test_into_doc() -> anyhow::Result<()> {
566        let collection_name = CollectionName::from_str(
567            "projects/my-project/databases/my-database/documents/chatrooms",
568        )?;
569        let document_name = collection_name.into_doc("chatroom1")?;
570        assert_eq!(
571            document_name,
572            DocumentName::from_str(
573                "projects/my-project/databases/my-database/documents/chatrooms/chatroom1"
574            )?
575        );
576
577        let collection_name = CollectionName::from_str(
578            "projects/my-project/databases/my-database/documents/chatrooms/chatroom1/messages",
579        )?;
580        let document_name = collection_name.into_doc("message1")?;
581        assert_eq!(
582            document_name,
583            DocumentName::from_str(
584                "projects/my-project/databases/my-database/documents/chatrooms/chatroom1/messages/message1"
585            )?
586        );
587
588        let collection_name = CollectionName::from_str(
589            "projects/my-project/databases/my-database/documents/chatrooms",
590        )?;
591        let document_id = DocumentId::from_str("chatroom1")?;
592        let document_name = collection_name.into_doc(document_id)?;
593        assert_eq!(
594            document_name,
595            DocumentName::from_str(
596                "projects/my-project/databases/my-database/documents/chatrooms/chatroom1"
597            )?
598        );
599
600        Ok(())
601    }
602
603    #[test]
604    fn test_parent() -> anyhow::Result<()> {
605        let s = "projects/my-project/databases/my-database/documents/chatrooms";
606        let collection_name = CollectionName::from_str(s)?;
607        assert_eq!(collection_name.into_parent(), None);
608
609        let s = "projects/my-project/databases/my-database/documents/chatrooms/chatroom1/messages";
610        let collection_name = CollectionName::from_str(s)?;
611        assert_eq!(
612            collection_name.into_parent(),
613            Some(DocumentName::from_str(
614                "projects/my-project/databases/my-database/documents/chatrooms/chatroom1"
615            )?)
616        );
617        Ok(())
618    }
619}