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}