firestore_path/
document_path.rs

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