robot_description_builder/
identifiers.rs

1//! TODO: GLOBAL DOCS
2//!
3//! TODO: FINISH DOCS OF THE MODULE
4//!
5//! # GroupID Delimiters
6//! # TODO: ADD FORMATTING AND ESCAPED CHARACTER EXPLANATION
7
8use std::fmt;
9
10/// The delimiter used at the start of a [`GroupID`].
11pub const DELIMITER_OPEN_GROUPID: &str = r"[[";
12/// The delimiter used at the end of a [`GroupID`].
13pub const DELIMITER_CLOSE_GROUPID: &str = r"]]";
14
15/// The escaped delimiter, which gets converted to [`DELIMITER_OPEN_GROUPID`] when applied.
16pub const DELIMITER_ESCAPED_OPEN_GROUPID: &str = r"[\[";
17/// The escaped delimiter, which gets converted to [`DELIMITER_CLOSE_GROUPID`] when applied.
18pub const DELIMITER_ESCAPED_CLOSE_GROUPID: &str = r"]\]";
19
20/// Enum to store the various types of errors that can cause invalidation of a [`GroupID`].
21///
22/// # Important
23/// When a validity check fails the error gets returned immediately,
24/// meaning that if it fails for multiple reasons only the first one is provided.
25/// This is the order the [`GroupID`] validity checks get performed:
26/// 1. Check for [`DELIMITER_OPEN_GROUPID`] ([`ContainsOpen`](`GroupIDErrorKind::ContainsOpen`))
27/// 2. Check for [`DELIMITER_CLOSE_GROUPID`] ([`ContainsClose`](`GroupIDErrorKind::ContainsClose`))
28/// 3. Check if non-empty ([`Empty`](`GroupIDErrorKind::Empty`))
29///
30/// # Example
31///
32/// ```
33/// # use robot_description_builder::identifiers::{GroupIDError, GroupID, GroupIDErrorKind};
34/// if let Err(e) = GroupID::is_valid_group_id(&"[[ThisIsInvalid]]") {
35///     println!("Invalid GroupID: {:?}", e.kind());
36/// #   assert_eq!(e.kind(), &GroupIDErrorKind::ContainsOpen);
37/// }
38/// # else { unreachable!() }
39/// ```
40#[derive(Debug, PartialEq, Eq, Clone)]
41pub enum GroupIDErrorKind {
42	/// `GroupID` being checked contains an unescaped opening `GroupID` delimiter.
43	///
44	/// This variant will be constructed when the [`GroupID`] being checked contains [`DELIMITER_OPEN_GROUPID`].
45	ContainsOpen,
46	/// `GroupID` being checked contains an unescaped closing `GroupID` delimiter.
47	///
48	/// This variant will be constructed when the [`GroupID`] being checked contains [`DELIMITER_CLOSE_GROUPID`].
49	ContainsClose,
50	/// `GroupID` being checked is empty.
51	///
52	/// This variant will be constructed when checking the [`GroupID`] validity of an empty string.
53	Empty,
54}
55
56/// An error which can be returned when checking for a [`GroupID`]'s validity.
57///
58/// This error is used as an error type for functions which check for [`GroupID`] validity such as [`GroupID::is_valid_group_id`]/
59///
60/// # TODO: Potential causes ?
61///
62/// # Example
63///
64/// ```
65/// # use robot_description_builder::identifiers::{GroupIDError, GroupID};
66/// if let Err(e) = GroupID::is_valid_group_id(&"[[no]]") {
67///     println!("Invalid GroupID: {e}");
68/// }
69/// # else { unreachable!() }
70/// ```
71#[derive(Debug, PartialEq, Eq, Clone)]
72pub struct GroupIDError {
73	/// The invalid [`GroupID`]
74	invalid_group_id: String,
75	/// The reason why the [`GroupID`] is invalid
76	pub(super) kind: GroupIDErrorKind,
77}
78
79impl GroupIDError {
80	/// Creates a [`GroupIDError`] of kind [`GroupIDErrorKind::ContainsOpen`]
81	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	/// Creates a [`GroupIDError`] of kind [`GroupIDErrorKind::ContainsClose`]
89	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	/// Creates a [`GroupIDError`] of kind [`GroupIDErrorKind::Empty`]
97	pub(super) fn new_empty() -> Self {
98		Self {
99			invalid_group_id: String::new(),
100			kind: GroupIDErrorKind::Empty,
101		}
102	}
103
104	/// Returns a reference to a cloned [`String`] of the [`GroupID`], which caused the error.
105	pub fn group_id(&self) -> &String {
106		&self.invalid_group_id
107	}
108
109	/// Outputs the detailed cause of invalidation of the [`GroupID`].
110	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
135/// Checks if the supplied `&str` is a valid [`GroupID`].
136///
137/// Returns a result type containing the valid `GroupID` as an `&str`, or an error with the invalidation reason.  
138fn check_group_id_validity(new_group_id: &str) -> Result<&str, GroupIDError> {
139	// !(new_group_id.contains(DELIMTER_OPEN_GROUPID)
140	// 	|| new_group_id.contains(DELIMTER_CLOSE_GROUPID)
141	// 	|| new_group_id.is_empty());
142
143	// TODO: Maybe also check for '"'
144
145	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
156/// Replaces the [`GroupID`] delimiters in the supplied `&str`
157///
158/// `replace_group_id_delimiters` creates a new `String`, and copies the data from the provided string slice into it.
159/// While doing so, it attempts to find matches of a the non-escaped and escapded `GroupID` delimiters.
160/// If it finds any, it replaces them with the replacements specified below.
161///
162/// The following replacements get made:
163///  - [`DELIMITER_OPEN_GROUPID`] with `""`
164///  - [`DELIMITER_CLOSE_GROUPID`] with `""`
165///  - [`DELIMITER_ESCAPED_OPEN_GROUPID`] with [`DELIMITER_OPEN_GROUPID`]
166///  - [`DELIMITER_ESCAPED_CLOSE_GROUPID`] with [`DELIMITER_CLOSE_GROUPID`]
167fn 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
175/// Format and validation trait for `GroupID`s
176///
177/// This trait is used to expand [`String`] and string slices for validity checks and `GroupID` escaped formatting applied
178///
179/// For more information on `GroupID` changing and escaping, see [the module-level documentation](`crate::identifiers`).
180///
181/// # TODO: Examples
182/// TODO: Maybe skip examples for this one
183pub trait GroupID {
184	/// Checks if the current `GroupID` is Valid
185	///
186	/// If the current [`GroupID`] is valid, the [`GroupID`] get's returned as `Ok(&str)`.
187	///
188	/// Otherwise an error of type [`GroupIDError`] is returned describing why the current [`GroupID`] is invalid.  
189	fn is_valid_group_id(&self) -> Result<&str, GroupIDError>;
190
191	/// Return an cloned `String` of the current `GroupID` with the delimiters replaced.
192	///
193	/// TODO: UPGRADE OR REFERENCE ANOTHER DOCUMENTATION TO PREVENT DISCRAPENCIES BETWEEN DOCS
194	///
195	/// Returns a cloned [`String`] with the following replacements:
196	///  - [`DELIMITER_OPEN_GROUPID`] with `""`
197	///  - [`DELIMITER_CLOSE_GROUPID`] with `""`
198	///  - [`DELIMITER_ESCAPED_OPEN_GROUPID`] with [`DELIMITER_OPEN_GROUPID`]
199	///  - [`DELIMITER_ESCAPED_CLOSE_GROUPID`] with [`DELIMITER_CLOSE_GROUPID`]
200	fn display(&self) -> String;
201
202	/// Maybe wrong place. TODO: Consider moving to `GroupIDChanger`
203	///
204	/// TODO:
205	/// - Move?
206	/// - Document
207	/// - Test
208	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	/// Maybe wrong place. TODO: Consider moving to `GroupIDChanger`
221	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	/// Maybe wrong place. TODO: Consider moving to `GroupIDChanger`
241	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
251/// Used for `GroupID` modifications on buildertrees.
252///
253/// Implementing this trait allows for the modification of identification string (often `name` field) of its implementor and his children.
254///
255/// The following operations can be done:
256///  - Replacing the [`GroupID`] section of the identification string.
257///  - Appling the [`GroupID` delimiter transformations](crate::identifiers#groupid-delimiters)
258///
259/// This should be achieved by recursively calling the desired method on the children of the implementor.
260///
261/// # Examples
262///
263/// Impemtation of `GroupIDChanger` for on an example struct tree:
264///
265/// ```
266/// use robot_description_builder::identifiers::{GroupIDChanger,GroupIDErrorKind};
267///
268/// #[derive(Debug, PartialEq, Eq, Clone)]
269/// struct ChildStruct {
270///     name: String
271/// }
272///
273/// impl GroupIDChanger for ChildStruct {
274///     unsafe fn change_group_id_unchecked(&mut self, new_group_id: &str) {
275///         self.name.change_group_id_unchecked(new_group_id);
276///     }
277///
278///     fn apply_group_id(&mut self) {
279///         self.name.apply_group_id();
280///     }
281/// }
282///
283/// #[derive(Debug, PartialEq, Eq, Clone)]
284/// struct ParentStruct {
285///     name: String,
286///     child: Option<ChildStruct>
287/// }
288///
289/// impl GroupIDChanger for ParentStruct {
290///     unsafe fn change_group_id_unchecked(&mut self, new_group_id: &str) {
291///         self.name.change_group_id_unchecked(new_group_id);
292///         if let Some(child) = self.child.as_mut() {
293///             child.change_group_id_unchecked(new_group_id);
294///         }
295///     }
296///
297///     fn apply_group_id(&mut self) {
298///         self.name.apply_group_id();
299///         if let Some(child) = self.child.as_mut() {
300///             child.apply_group_id();
301///         }
302///     }
303/// }
304///
305/// let example_tree = ParentStruct{
306///         name: "tree_[[0]]".into(),
307///         child: Some(ChildStruct{name:"tree_child_[[0]][\\[".into()})
308///     };
309///
310/// // Appling a GroupID
311/// let mut applied_tree = example_tree.clone();
312/// applied_tree.apply_group_id();
313/// assert_eq!(
314///     applied_tree,
315///     ParentStruct{
316///         name: "tree_0".into(),
317///         child: Some(ChildStruct{name:"tree_child_0[[".into()})
318///     }
319/// );
320///
321/// // Changing the GroupID
322/// let mut changed_tree = example_tree.clone();
323/// assert!(changed_tree.change_group_id("1").is_ok());
324/// assert_eq!(
325///     changed_tree,
326///     ParentStruct{
327///         name: "tree_[[1]]".into(),
328///         child: Some(ChildStruct{name:"tree_child_[[1]][\\[".into()})
329///     }
330/// );
331///
332/// // Invalid GroupID
333/// let mut failed_tree = example_tree.clone();
334/// assert_eq!(changed_tree.change_group_id("").unwrap_err().kind(), &GroupIDErrorKind::Empty);
335/// // The tree remains unchanged
336/// assert_eq!(failed_tree, example_tree);
337/// ```
338pub trait GroupIDChanger {
339	/// Replaces the `GroupID` of the builder tree with `new_group_id`.
340	///
341	/// If `new_group_id` is a valid [`GroupID`] then the `GroupID` of the whole buildertree is replaced.
342	/// Otherwise, this method fails returning an error explaing the invalidation.
343	///
344	/// For performance reasons the check only get's performed here,
345	/// when this succeeds [`change_group_id_unchecked`][GroupIDChanger::change_group_id_unchecked] is used to perform the actual updating.
346	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	/// Unchecked replacement of the `GroupID` of the builder tree with `new_group_id`.
354	///
355	/// Changes the [`GroupID`] of the identification string of the current builder tree without checking if the `new_group_id` is valid.
356	/// This should be achieved by calling this method on all its implementors childeren and its identification string often called `name`.
357	///
358	/// # Safety
359	///
360	/// This function should be called with a valid [`GroupID`].
361	/// It is recommended to use [`change_group_id`](GroupIDChanger::change_group_id) instead.
362	unsafe fn change_group_id_unchecked(&mut self, new_group_id: &str);
363
364	/// Applies `GroupID` delimiter replacements.
365	///
366	/// Replaces the [`GroupID`] delimiters in the current builder tree.
367	///
368	/// TODO: REFERENCE MODULE DOC ABOUT GroupID Delimiters and replacements
369	///
370	/// -----
371	/// TODO: UPGRADE
372	///
373	/// Replaces:
374	///  - [`DELIMITER_OPEN_GROUPID`] with `""`
375	///  - [`DELIMITER_CLOSE_GROUPID`] with `""`
376	///  - [`DELIMITER_ESCAPED_OPEN_GROUPID`] with [`DELIMITER_OPEN_GROUPID`]
377	///  - [`DELIMITER_ESCAPED_CLOSE_GROUPID`] with [`DELIMITER_CLOSE_GROUPID`]
378	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		// Maybe checking is uncessesary
419		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		// Delimiters
506		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		// Escaped
517		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		// Mixed
527		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		/// GroupID::is_valid_group_id()
539		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			// Delimiters
582			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			// Escaped
591			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			// Mixed
601			assert_eq!(
602				"multi_groupid_Leg_[\\[L04]\\]_Claw_[[L01]]".display(),
603				"multi_groupid_Leg_[[L04]]_Claw_L01"
604			);
605			// Mixed
606			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			// Delimiters
632			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				// TODO: Is this final behavior?
636				"Before[[[[Anything]]]]After",
637				"Sunrise",
638				"Before[[[[Anything]]]]After", // "BeforeSunriseAfter",
639			);
640
641			// Escaped
642			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			// Mixed
654			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			// UNCHECKED BEHAVIOR
666			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			// Delimiters
694			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				// TODO: Is this final behavior?
698				"Before[[[[Anything]]]]After",
699				"Sunrise",
700				Ok(()),
701				"Before[[[[Anything]]]]After", // "BeforeSunriseAfter",
702			);
703
704			// Escaped
705			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			// Mixed
719			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			// UNCHECKED BEHAVIOR
733			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			// Delimiters
764			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				// TODO: Is this final behavior?
768				"Before[[[[Anything]]]]After",
769				"Before[[[[Anything]]]]After",
770			);
771
772			// Escaped
773			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			// Mixed
783			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}