1use std::fmt;
9
10pub const DELIMITER_OPEN_GROUPID: &str = r"[[";
12pub const DELIMITER_CLOSE_GROUPID: &str = r"]]";
14
15pub const DELIMITER_ESCAPED_OPEN_GROUPID: &str = r"[\[";
17pub const DELIMITER_ESCAPED_CLOSE_GROUPID: &str = r"]\]";
19
20#[derive(Debug, PartialEq, Eq, Clone)]
41pub enum GroupIDErrorKind {
42 ContainsOpen,
46 ContainsClose,
50 Empty,
54}
55
56#[derive(Debug, PartialEq, Eq, Clone)]
72pub struct GroupIDError {
73 invalid_group_id: String,
75 pub(super) kind: GroupIDErrorKind,
77}
78
79impl GroupIDError {
80 pub(super) fn new_open(invalid_group_id: &str) -> Self {
82 Self {
83 invalid_group_id: invalid_group_id.to_string(),
84 kind: GroupIDErrorKind::ContainsOpen,
85 }
86 }
87
88 pub(super) fn new_close(invalid_group_id: &str) -> Self {
90 Self {
91 invalid_group_id: invalid_group_id.to_string(),
92 kind: GroupIDErrorKind::ContainsClose,
93 }
94 }
95
96 pub(super) fn new_empty() -> Self {
98 Self {
99 invalid_group_id: String::new(),
100 kind: GroupIDErrorKind::Empty,
101 }
102 }
103
104 pub fn group_id(&self) -> &String {
106 &self.invalid_group_id
107 }
108
109 pub fn kind(&self) -> &GroupIDErrorKind {
111 &self.kind
112 }
113}
114
115impl fmt::Display for GroupIDError {
116 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
117 match self.kind {
118 GroupIDErrorKind::ContainsOpen => write!(
119 f,
120 "invalid opening delimter (\"{}\") found in GroupID (\"{}\")",
121 DELIMITER_OPEN_GROUPID, self.invalid_group_id
122 ),
123 GroupIDErrorKind::ContainsClose => write!(
124 f,
125 "invalid closing delimiter (\"{}\") found in GroupID (\"{}\")",
126 DELIMITER_CLOSE_GROUPID, self.invalid_group_id
127 ),
128 GroupIDErrorKind::Empty => write!(f, "cannot change GroupID to empty string"),
129 }
130 }
131}
132
133impl std::error::Error for GroupIDError {}
134
135fn check_group_id_validity(new_group_id: &str) -> Result<&str, GroupIDError> {
139 if new_group_id.contains(DELIMITER_OPEN_GROUPID) {
146 Err(GroupIDError::new_open(new_group_id))
147 } else if new_group_id.contains(DELIMITER_CLOSE_GROUPID) {
148 Err(GroupIDError::new_close(new_group_id))
149 } else if new_group_id.is_empty() {
150 Err(GroupIDError::new_empty())
151 } else {
152 Ok(new_group_id)
153 }
154}
155
156fn replace_group_id_delimiters(input: &str) -> String {
168 input
169 .replace(DELIMITER_OPEN_GROUPID, "")
170 .replace(DELIMITER_CLOSE_GROUPID, "")
171 .replace(DELIMITER_ESCAPED_OPEN_GROUPID, DELIMITER_OPEN_GROUPID)
172 .replace(DELIMITER_ESCAPED_CLOSE_GROUPID, DELIMITER_CLOSE_GROUPID)
173}
174
175pub trait GroupID {
184 fn is_valid_group_id(&self) -> Result<&str, GroupIDError>;
190
191 fn display(&self) -> String;
201
202 fn get_group_id(&self) -> Option<&str>;
209}
210
211impl GroupID for String {
212 fn is_valid_group_id(&self) -> Result<&str, GroupIDError> {
213 check_group_id_validity(self)
214 }
215
216 fn display(&self) -> String {
217 replace_group_id_delimiters(self)
218 }
219
220 fn get_group_id(&self) -> Option<&str> {
222 self.split_once(DELIMITER_OPEN_GROUPID)
223 .and_then(|(_, near_group_id)| {
224 near_group_id
225 .rsplit_once(DELIMITER_CLOSE_GROUPID)
226 .map(|(group_id, _)| group_id)
227 })
228 }
229}
230
231impl GroupID for &str {
232 fn is_valid_group_id(&self) -> Result<&str, GroupIDError> {
233 check_group_id_validity(self)
234 }
235
236 fn display(&self) -> String {
237 replace_group_id_delimiters(self)
238 }
239
240 fn get_group_id(&self) -> Option<&str> {
242 self.split_once(DELIMITER_OPEN_GROUPID)
243 .and_then(|(_, near_group_id)| {
244 near_group_id
245 .rsplit_once(DELIMITER_CLOSE_GROUPID)
246 .map(|(group_id, _)| group_id)
247 })
248 }
249}
250
251pub trait GroupIDChanger {
339 fn change_group_id(&mut self, new_group_id: impl GroupID) -> Result<(), GroupIDError> {
347 unsafe {
348 Self::change_group_id_unchecked(self, new_group_id.is_valid_group_id()?);
349 };
350 Ok(())
351 }
352
353 unsafe fn change_group_id_unchecked(&mut self, new_group_id: &str);
363
364 fn apply_group_id(&mut self);
379}
380
381impl GroupIDChanger for String {
382 unsafe fn change_group_id_unchecked(&mut self, new_group_id: &str) {
383 if self.matches(DELIMITER_OPEN_GROUPID).count() == 1
384 && self.matches(DELIMITER_CLOSE_GROUPID).count() == 1
385 {
386 if let Some((pre, _, post)) =
387 self.split_once(DELIMITER_OPEN_GROUPID)
388 .and_then(|(pre, remainder)| {
389 remainder
390 .split_once(DELIMITER_CLOSE_GROUPID)
391 .map(|(group_id, post)| (pre, group_id, post))
392 }) {
393 let new = format!(
394 "{}{}{}{}{}",
395 pre, DELIMITER_OPEN_GROUPID, new_group_id, DELIMITER_CLOSE_GROUPID, post
396 );
397
398 #[cfg(any(feature = "logging", test))]
399 log::info!(
400 target: "GroupIDChanger",
401 "The identification string \"{}\" was replaced by \"{}\"",
402 self, new
403 );
404
405 *self = new;
406 }
407 } else {
408 #[cfg(any(feature = "logging", test))]
409 log::info!(
410 target: "GroupIDChanger",
411 "The changing of the GroupID of \"{}\" was skipped due to not having exactly 1 opening and 1 closing delimiter",
412 self
413 );
414 }
415 }
416
417 fn apply_group_id(&mut self) {
418 let open_count = self.matches(DELIMITER_OPEN_GROUPID).count();
420 let close_count = self.matches(DELIMITER_CLOSE_GROUPID).count();
421
422 if (open_count == 1 && close_count == 1) || (open_count == 0 && close_count == 0) {
423 let new = Self::display(self);
424
425 #[cfg(any(feature = "logging", test))]
426 log::info!(
427 target: "GroupIDChanger",
428 "Applied GroupID delimiter transformations to \"{}\", changed to \"{}\"",
429 self, new
430 );
431
432 *self = new;
433 } else {
434 #[cfg(any(feature = "logging", test))]
435 log::info!(
436 target: "GroupIDChanger",
437 "The GroupID delimiters transformations where not applied to \"{}\", because {}",
438 self,
439 match (open_count, close_count) {
440 (0, 0) | (1, 1) => unreachable!(),
441 (1, 0) => format!("of an unclosed GroupID field. (missing \"{DELIMITER_CLOSE_GROUPID}\")"),
442 (0, 1) => format!("of an unopened GroupID field. (missing \"{DELIMITER_OPEN_GROUPID}\")"),
443 (0 | 1, _) => format!("of excess closing delimeters (\"{DELIMITER_CLOSE_GROUPID}\"), expected {open_count} closing tags based on amount of opening tags, got {close_count} closing tags"),
444 (_, 0 | 1) => format!("of excess opening delimeters (\"{DELIMITER_OPEN_GROUPID}\"), expected {close_count} opening tags based on amount of closing tags, got {open_count} opening tags"),
445 (_, _) => format!("of unexpected amount of opening and closing tags, got (Open, close) = ({open_count}, {close_count}), expected (0, 0) or (1, 1)")
446 }
447 );
448 }
449 }
450}
451
452#[cfg(test)]
453mod tests {
454 use super::{
455 check_group_id_validity, replace_group_id_delimiters, GroupIDError, GroupIDErrorKind,
456 DELIMITER_ESCAPED_CLOSE_GROUPID, DELIMITER_ESCAPED_OPEN_GROUPID,
457 };
458 use test_log::test;
459
460 #[test]
461 fn test_check_group_id_validity() {
462 assert_eq!(
463 check_group_id_validity("[[---"),
464 Err(GroupIDError {
465 invalid_group_id: "[[---".to_string(),
466 kind: GroupIDErrorKind::ContainsOpen
467 })
468 );
469
470 assert_eq!(
471 check_group_id_validity("smiley? :]]"),
472 Err(GroupIDError {
473 invalid_group_id: "smiley? :]]".to_string(),
474 kind: GroupIDErrorKind::ContainsClose
475 })
476 );
477
478 assert_eq!(
479 check_group_id_validity(""),
480 Err(GroupIDError {
481 invalid_group_id: String::new(),
482 kind: GroupIDErrorKind::Empty
483 })
484 );
485
486 assert_eq!(check_group_id_validity("L02"), Ok("L02"));
487 assert_eq!(check_group_id_validity("left_arm"), Ok("left_arm"));
488 assert_eq!(
489 check_group_id_validity(&String::from("Left[4]")),
490 Ok("Left[4]")
491 );
492 assert_eq!(
493 check_group_id_validity(&format!(
494 "Right{}99999999999999{}_final_count_down",
495 DELIMITER_ESCAPED_OPEN_GROUPID, DELIMITER_ESCAPED_CLOSE_GROUPID
496 )),
497 Ok(r#"Right[\[99999999999999]\]_final_count_down"#)
498 );
499 }
500
501 #[test]
502 fn test_replace_group_id_delimiters() {
503 assert_eq!(replace_group_id_delimiters("nothing"), "nothing");
504
505 assert_eq!(
507 replace_group_id_delimiters("[[Hopefully Not Hidden]]"),
508 "Hopefully Not Hidden"
509 );
510 assert_eq!(replace_group_id_delimiters("colo[[[u]]]r"), "colo[u]r");
511 assert_eq!(
512 replace_group_id_delimiters("Before[[[[Anything]]]]After"),
513 "BeforeAnythingAfter"
514 );
515
516 assert_eq!(
518 replace_group_id_delimiters("Obsidian Internal Link [\\[Anything]\\]"),
519 "Obsidian Internal Link [[Anything]]"
520 );
521 assert_eq!(
522 replace_group_id_delimiters("Front[\\[:[\\[Center]\\]:]\\]Back"),
523 "Front[[:[[Center]]:]]Back"
524 );
525
526 assert_eq!(
528 replace_group_id_delimiters("multi_groupid_Leg_[\\[L04]\\]_Claw_[[L01]]"),
529 "multi_groupid_Leg_[[L04]]_Claw_L01"
530 );
531 }
532
533 mod group_id {
534 use super::{test, DELIMITER_ESCAPED_CLOSE_GROUPID, DELIMITER_ESCAPED_OPEN_GROUPID};
535 use crate::identifiers::{GroupID, GroupIDError, GroupIDErrorKind};
536
537 #[test]
538 fn is_valid_group_id() {
540 assert_eq!(
541 "[[---".is_valid_group_id(),
542 Err(GroupIDError {
543 invalid_group_id: "[[---".to_string(),
544 kind: GroupIDErrorKind::ContainsOpen
545 })
546 );
547
548 assert_eq!(
549 "smiley? :]]".is_valid_group_id(),
550 Err(GroupIDError {
551 invalid_group_id: "smiley? :]]".to_string(),
552 kind: GroupIDErrorKind::ContainsClose
553 })
554 );
555
556 assert_eq!(
557 "".is_valid_group_id(),
558 Err(GroupIDError {
559 invalid_group_id: String::new(),
560 kind: GroupIDErrorKind::Empty
561 })
562 );
563
564 assert_eq!("L02".is_valid_group_id(), Ok("L02"));
565 assert_eq!("left_arm".is_valid_group_id(), Ok("left_arm"));
566 assert_eq!("Left[4]".is_valid_group_id(), Ok("Left[4]"));
567 assert_eq!(
568 format!(
569 "Right{}99999999999999{}_final_count_down",
570 DELIMITER_ESCAPED_OPEN_GROUPID, DELIMITER_ESCAPED_CLOSE_GROUPID
571 )
572 .is_valid_group_id(),
573 Ok(r#"Right[\[99999999999999]\]_final_count_down"#)
574 );
575 }
576
577 #[test]
578 fn display() {
579 assert_eq!("nothing".display(), "nothing");
580
581 assert_eq!("[[Hopefully Not Hidden]]".display(), "Hopefully Not Hidden");
583 assert_eq!("colo[[[u]]]r".display(), "colo[u]r");
584 assert_eq!("colo[[[u]]]r".to_string().display(), "colo[u]r");
585 assert_eq!(
586 "Before[[[[Anything]]]]After".display(),
587 "BeforeAnythingAfter"
588 );
589
590 assert_eq!(
592 "Obsidian Internal Link [\\[Anything]\\]".display(),
593 "Obsidian Internal Link [[Anything]]"
594 );
595 assert_eq!(
596 "Front[\\[:[\\[Center]\\]:]\\]Back".display(),
597 "Front[[:[[Center]]:]]Back"
598 );
599
600 assert_eq!(
602 "multi_groupid_Leg_[\\[L04]\\]_Claw_[[L01]]".display(),
603 "multi_groupid_Leg_[[L04]]_Claw_L01"
604 );
605 assert_eq!(
607 "multi_groupid_Leg_[\\[L04]\\]_Claw_[[L01]]"
608 .to_string()
609 .display(),
610 "multi_groupid_Leg_[[L04]]_Claw_L01"
611 );
612 }
613 }
614
615 mod group_id_changer {
616 use super::test;
617 use crate::identifiers::{GroupIDChanger, GroupIDError, GroupIDErrorKind};
618
619 fn test_change_group_id_unchecked(s: impl Into<String>, new_group_id: &str, result: &str) {
620 let mut s: String = s.into();
621 unsafe {
622 s.change_group_id_unchecked(new_group_id);
623 }
624 assert_eq!(s, result)
625 }
626
627 #[test]
628 fn change_group_id_unchecked() {
629 test_change_group_id_unchecked("nothing", "R02", "nothing");
630
631 test_change_group_id_unchecked("[[Hopefully Not Hidden]]", "R02", "[[R02]]");
633 test_change_group_id_unchecked("colo[[[u]]]r", "u", "colo[[u]]]r");
634 test_change_group_id_unchecked(
635 "Before[[[[Anything]]]]After",
637 "Sunrise",
638 "Before[[[[Anything]]]]After", );
640
641 test_change_group_id_unchecked(
643 "Obsidian Internal Link [\\[Anything]\\]",
644 ".....",
645 "Obsidian Internal Link [\\[Anything]\\]",
646 );
647 test_change_group_id_unchecked(
648 "Front[\\[:[\\[Center]\\]:]\\]Back",
649 ".....",
650 "Front[\\[:[\\[Center]\\]:]\\]Back",
651 );
652
653 test_change_group_id_unchecked(
655 "multi_groupid_Leg_[\\[L04]\\]_Claw_[[L01]]",
656 "R09",
657 "multi_groupid_Leg_[\\[L04]\\]_Claw_[[R09]]",
658 );
659 test_change_group_id_unchecked(
660 "Front[\\[:[[Center]]:]\\]Back",
661 "Middle",
662 "Front[\\[:[[Middle]]:]\\]Back",
663 );
664
665 test_change_group_id_unchecked(
667 "multi_groupid_Leg_[\\[L04]\\]_Claw_[[L01]]",
668 "[[R08]]",
669 "multi_groupid_Leg_[\\[L04]\\]_Claw_[[[[R08]]]]",
670 );
671 test_change_group_id_unchecked(
672 "Front[\\[:[[Center]]:]\\]Back",
673 "",
674 "Front[\\[:[[]]:]\\]Back",
675 );
676 }
677
678 fn test_change_group_id(
679 s: impl Into<String>,
680 new_group_id: &str,
681 func_result: Result<(), GroupIDError>,
682 new_identifier: &str,
683 ) {
684 let mut s: String = s.into();
685 assert_eq!(s.change_group_id(new_group_id), func_result);
686 assert_eq!(s, new_identifier);
687 }
688
689 #[test]
690 fn change_group_id() {
691 test_change_group_id("nothing", "R02", Ok(()), "nothing");
692
693 test_change_group_id("[[Hopefully Not Hidden]]", "R02", Ok(()), "[[R02]]");
695 test_change_group_id("colo[[[u]]]r", "u", Ok(()), "colo[[u]]]r");
696 test_change_group_id(
697 "Before[[[[Anything]]]]After",
699 "Sunrise",
700 Ok(()),
701 "Before[[[[Anything]]]]After", );
703
704 test_change_group_id(
706 "Obsidian Internal Link [\\[Anything]\\]",
707 ".....",
708 Ok(()),
709 "Obsidian Internal Link [\\[Anything]\\]",
710 );
711 test_change_group_id(
712 "Front[\\[:[\\[Center]\\]:]\\]Back",
713 ".....",
714 Ok(()),
715 "Front[\\[:[\\[Center]\\]:]\\]Back",
716 );
717
718 test_change_group_id(
720 "multi_groupid_Leg_[\\[L04]\\]_Claw_[[L01]]",
721 "R09",
722 Ok(()),
723 "multi_groupid_Leg_[\\[L04]\\]_Claw_[[R09]]",
724 );
725 test_change_group_id(
726 "Front[\\[:[[Center]]:]\\]Back",
727 "Middle",
728 Ok(()),
729 "Front[\\[:[[Middle]]:]\\]Back",
730 );
731
732 test_change_group_id(
734 "multi_groupid_Leg_[\\[L04]\\]_Claw_[[L01]]",
735 "[[R08]]",
736 Err(GroupIDError {
737 invalid_group_id: "[[R08]]".into(),
738 kind: GroupIDErrorKind::ContainsOpen,
739 }),
740 "multi_groupid_Leg_[\\[L04]\\]_Claw_[[L01]]",
741 );
742 test_change_group_id(
743 "Front[\\[:[[Center]]:]\\]Back",
744 "",
745 Err(GroupIDError {
746 invalid_group_id: String::new(),
747 kind: GroupIDErrorKind::Empty,
748 }),
749 "Front[\\[:[[Center]]:]\\]Back",
750 );
751 }
752
753 fn test_apply_group_id(s: impl Into<String>, result: &str) {
754 let mut s: String = s.into();
755 s.apply_group_id();
756 assert_eq!(s, result);
757 }
758
759 #[test]
760 fn apply_group_id() {
761 test_apply_group_id("nothing", "nothing");
762
763 test_apply_group_id("[[Hopefully Not Hidden]]", "Hopefully Not Hidden");
765 test_apply_group_id("colo[[[u]]]r", "colo[u]r");
766 test_apply_group_id(
767 "Before[[[[Anything]]]]After",
769 "Before[[[[Anything]]]]After",
770 );
771
772 test_apply_group_id(
774 "Obsidian Internal Link [\\[Anything]\\]",
775 "Obsidian Internal Link [[Anything]]",
776 );
777 test_apply_group_id(
778 "Front[\\[:[\\[Center]\\]:]\\]Back",
779 "Front[[:[[Center]]:]]Back",
780 );
781
782 test_apply_group_id(
784 "multi_groupid_Leg_[\\[L04]\\]_Claw_[[L01]]",
785 "multi_groupid_Leg_[[L04]]_Claw_L01",
786 );
787 test_apply_group_id("Front[\\[:[[Center]]:]\\]Back", "Front[[:Center:]]Back");
788
789 test_apply_group_id(
790 "multi_groupid_Leg_[\\[L04]\\]]_Claw_[[L01]]",
791 "multi_groupid_Leg_[\\[L04]\\]]_Claw_[[L01]]",
792 );
793 }
794 }
795}