firestore_path/
collection_path.rs

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