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}