use std::fmt;
pub const DELIMITER_OPEN_GROUPID: &str = r"[[";
pub const DELIMITER_CLOSE_GROUPID: &str = r"]]";
pub const DELIMITER_ESCAPED_OPEN_GROUPID: &str = r"[\[";
pub const DELIMITER_ESCAPED_CLOSE_GROUPID: &str = r"]\]";
#[derive(Debug, PartialEq, Eq, Clone)]
pub enum GroupIDErrorKind {
ContainsOpen,
ContainsClose,
Empty,
}
#[derive(Debug, PartialEq, Eq, Clone)]
pub struct GroupIDError {
invalid_group_id: String,
pub(super) kind: GroupIDErrorKind,
}
impl GroupIDError {
pub(super) fn new_open(invalid_group_id: &str) -> Self {
Self {
invalid_group_id: invalid_group_id.to_string(),
kind: GroupIDErrorKind::ContainsOpen,
}
}
pub(super) fn new_close(invalid_group_id: &str) -> Self {
Self {
invalid_group_id: invalid_group_id.to_string(),
kind: GroupIDErrorKind::ContainsClose,
}
}
pub(super) fn new_empty() -> Self {
Self {
invalid_group_id: String::new(),
kind: GroupIDErrorKind::Empty,
}
}
pub fn group_id(&self) -> &String {
&self.invalid_group_id
}
pub fn kind(&self) -> &GroupIDErrorKind {
&self.kind
}
}
impl fmt::Display for GroupIDError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self.kind {
GroupIDErrorKind::ContainsOpen => write!(
f,
"invalid opening delimter (\"{}\") found in GroupID (\"{}\")",
DELIMITER_OPEN_GROUPID, self.invalid_group_id
),
GroupIDErrorKind::ContainsClose => write!(
f,
"invalid closing delimiter (\"{}\") found in GroupID (\"{}\")",
DELIMITER_CLOSE_GROUPID, self.invalid_group_id
),
GroupIDErrorKind::Empty => write!(f, "cannot change GroupID to empty string"),
}
}
}
impl std::error::Error for GroupIDError {}
fn check_group_id_validity(new_group_id: &str) -> Result<&str, GroupIDError> {
if new_group_id.contains(DELIMITER_OPEN_GROUPID) {
Err(GroupIDError::new_open(new_group_id))
} else if new_group_id.contains(DELIMITER_CLOSE_GROUPID) {
Err(GroupIDError::new_close(new_group_id))
} else if new_group_id.is_empty() {
Err(GroupIDError::new_empty())
} else {
Ok(new_group_id)
}
}
fn replace_group_id_delimiters(input: &str) -> String {
input
.replace(DELIMITER_OPEN_GROUPID, "")
.replace(DELIMITER_CLOSE_GROUPID, "")
.replace(DELIMITER_ESCAPED_OPEN_GROUPID, DELIMITER_OPEN_GROUPID)
.replace(DELIMITER_ESCAPED_CLOSE_GROUPID, DELIMITER_CLOSE_GROUPID)
}
pub trait GroupID {
fn is_valid_group_id(&self) -> Result<&str, GroupIDError>;
fn display(&self) -> String;
fn get_group_id(&self) -> Option<&str>;
}
impl GroupID for String {
fn is_valid_group_id(&self) -> Result<&str, GroupIDError> {
check_group_id_validity(self)
}
fn display(&self) -> String {
replace_group_id_delimiters(self)
}
fn get_group_id(&self) -> Option<&str> {
self.split_once(DELIMITER_OPEN_GROUPID)
.and_then(|(_, near_group_id)| {
near_group_id
.rsplit_once(DELIMITER_CLOSE_GROUPID)
.map(|(group_id, _)| group_id)
})
}
}
impl GroupID for &str {
fn is_valid_group_id(&self) -> Result<&str, GroupIDError> {
check_group_id_validity(self)
}
fn display(&self) -> String {
replace_group_id_delimiters(self)
}
fn get_group_id(&self) -> Option<&str> {
self.split_once(DELIMITER_OPEN_GROUPID)
.and_then(|(_, near_group_id)| {
near_group_id
.rsplit_once(DELIMITER_CLOSE_GROUPID)
.map(|(group_id, _)| group_id)
})
}
}
pub trait GroupIDChanger {
fn change_group_id(&mut self, new_group_id: impl GroupID) -> Result<(), GroupIDError> {
unsafe {
Self::change_group_id_unchecked(self, new_group_id.is_valid_group_id()?);
};
Ok(())
}
unsafe fn change_group_id_unchecked(&mut self, new_group_id: &str);
fn apply_group_id(&mut self);
}
impl GroupIDChanger for String {
unsafe fn change_group_id_unchecked(&mut self, new_group_id: &str) {
if self.matches(DELIMITER_OPEN_GROUPID).count() == 1
&& self.matches(DELIMITER_CLOSE_GROUPID).count() == 1
{
if let Some((pre, _, post)) =
self.split_once(DELIMITER_OPEN_GROUPID)
.and_then(|(pre, remainder)| {
remainder
.split_once(DELIMITER_CLOSE_GROUPID)
.map(|(group_id, post)| (pre, group_id, post))
}) {
let new = format!(
"{}{}{}{}{}",
pre, DELIMITER_OPEN_GROUPID, new_group_id, DELIMITER_CLOSE_GROUPID, post
);
#[cfg(any(feature = "logging", test))]
log::info!(
target: "GroupIDChanger",
"The identification string \"{}\" was replaced by \"{}\"",
self, new
);
*self = new;
}
} else {
#[cfg(any(feature = "logging", test))]
log::info!(
target: "GroupIDChanger",
"The changing of the GroupID of \"{}\" was skipped due to not having exactly 1 opening and 1 closing delimiter",
self
);
}
}
fn apply_group_id(&mut self) {
let open_count = self.matches(DELIMITER_OPEN_GROUPID).count();
let close_count = self.matches(DELIMITER_CLOSE_GROUPID).count();
if (open_count == 1 && close_count == 1) || (open_count == 0 && close_count == 0) {
let new = Self::display(self);
#[cfg(any(feature = "logging", test))]
log::info!(
target: "GroupIDChanger",
"Applied GroupID delimiter transformations to \"{}\", changed to \"{}\"",
self, new
);
*self = new;
} else {
#[cfg(any(feature = "logging", test))]
log::info!(
target: "GroupIDChanger",
"The GroupID delimiters transformations where not applied to \"{}\", because {}",
self,
match (open_count, close_count) {
(0, 0) | (1, 1) => unreachable!(),
(1, 0) => format!("of an unclosed GroupID field. (missing \"{DELIMITER_CLOSE_GROUPID}\")"),
(0, 1) => format!("of an unopened GroupID field. (missing \"{DELIMITER_OPEN_GROUPID}\")"),
(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"),
(_, 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"),
(_, _) => format!("of unexpected amount of opening and closing tags, got (Open, close) = ({open_count}, {close_count}), expected (0, 0) or (1, 1)")
}
);
}
}
}
#[cfg(test)]
mod tests {
use super::{
check_group_id_validity, replace_group_id_delimiters, GroupIDError, GroupIDErrorKind,
DELIMITER_ESCAPED_CLOSE_GROUPID, DELIMITER_ESCAPED_OPEN_GROUPID,
};
use test_log::test;
#[test]
fn test_check_group_id_validity() {
assert_eq!(
check_group_id_validity("[[---"),
Err(GroupIDError {
invalid_group_id: "[[---".to_string(),
kind: GroupIDErrorKind::ContainsOpen
})
);
assert_eq!(
check_group_id_validity("smiley? :]]"),
Err(GroupIDError {
invalid_group_id: "smiley? :]]".to_string(),
kind: GroupIDErrorKind::ContainsClose
})
);
assert_eq!(
check_group_id_validity(""),
Err(GroupIDError {
invalid_group_id: String::new(),
kind: GroupIDErrorKind::Empty
})
);
assert_eq!(check_group_id_validity("L02"), Ok("L02"));
assert_eq!(check_group_id_validity("left_arm"), Ok("left_arm"));
assert_eq!(
check_group_id_validity(&String::from("Left[4]")),
Ok("Left[4]")
);
assert_eq!(
check_group_id_validity(&format!(
"Right{}99999999999999{}_final_count_down",
DELIMITER_ESCAPED_OPEN_GROUPID, DELIMITER_ESCAPED_CLOSE_GROUPID
)),
Ok(r#"Right[\[99999999999999]\]_final_count_down"#)
);
}
#[test]
fn test_replace_group_id_delimiters() {
assert_eq!(replace_group_id_delimiters("nothing"), "nothing");
assert_eq!(
replace_group_id_delimiters("[[Hopefully Not Hidden]]"),
"Hopefully Not Hidden"
);
assert_eq!(replace_group_id_delimiters("colo[[[u]]]r"), "colo[u]r");
assert_eq!(
replace_group_id_delimiters("Before[[[[Anything]]]]After"),
"BeforeAnythingAfter"
);
assert_eq!(
replace_group_id_delimiters("Obsidian Internal Link [\\[Anything]\\]"),
"Obsidian Internal Link [[Anything]]"
);
assert_eq!(
replace_group_id_delimiters("Front[\\[:[\\[Center]\\]:]\\]Back"),
"Front[[:[[Center]]:]]Back"
);
assert_eq!(
replace_group_id_delimiters("multi_groupid_Leg_[\\[L04]\\]_Claw_[[L01]]"),
"multi_groupid_Leg_[[L04]]_Claw_L01"
);
}
mod group_id {
use super::{test, DELIMITER_ESCAPED_CLOSE_GROUPID, DELIMITER_ESCAPED_OPEN_GROUPID};
use crate::identifiers::{GroupID, GroupIDError, GroupIDErrorKind};
#[test]
fn is_valid_group_id() {
assert_eq!(
"[[---".is_valid_group_id(),
Err(GroupIDError {
invalid_group_id: "[[---".to_string(),
kind: GroupIDErrorKind::ContainsOpen
})
);
assert_eq!(
"smiley? :]]".is_valid_group_id(),
Err(GroupIDError {
invalid_group_id: "smiley? :]]".to_string(),
kind: GroupIDErrorKind::ContainsClose
})
);
assert_eq!(
"".is_valid_group_id(),
Err(GroupIDError {
invalid_group_id: String::new(),
kind: GroupIDErrorKind::Empty
})
);
assert_eq!("L02".is_valid_group_id(), Ok("L02"));
assert_eq!("left_arm".is_valid_group_id(), Ok("left_arm"));
assert_eq!("Left[4]".is_valid_group_id(), Ok("Left[4]"));
assert_eq!(
format!(
"Right{}99999999999999{}_final_count_down",
DELIMITER_ESCAPED_OPEN_GROUPID, DELIMITER_ESCAPED_CLOSE_GROUPID
)
.is_valid_group_id(),
Ok(r#"Right[\[99999999999999]\]_final_count_down"#)
);
}
#[test]
fn display() {
assert_eq!("nothing".display(), "nothing");
assert_eq!("[[Hopefully Not Hidden]]".display(), "Hopefully Not Hidden");
assert_eq!("colo[[[u]]]r".display(), "colo[u]r");
assert_eq!("colo[[[u]]]r".to_string().display(), "colo[u]r");
assert_eq!(
"Before[[[[Anything]]]]After".display(),
"BeforeAnythingAfter"
);
assert_eq!(
"Obsidian Internal Link [\\[Anything]\\]".display(),
"Obsidian Internal Link [[Anything]]"
);
assert_eq!(
"Front[\\[:[\\[Center]\\]:]\\]Back".display(),
"Front[[:[[Center]]:]]Back"
);
assert_eq!(
"multi_groupid_Leg_[\\[L04]\\]_Claw_[[L01]]".display(),
"multi_groupid_Leg_[[L04]]_Claw_L01"
);
assert_eq!(
"multi_groupid_Leg_[\\[L04]\\]_Claw_[[L01]]"
.to_string()
.display(),
"multi_groupid_Leg_[[L04]]_Claw_L01"
);
}
}
mod group_id_changer {
use super::test;
use crate::identifiers::{GroupIDChanger, GroupIDError, GroupIDErrorKind};
fn test_change_group_id_unchecked(s: impl Into<String>, new_group_id: &str, result: &str) {
let mut s: String = s.into();
unsafe {
s.change_group_id_unchecked(new_group_id);
}
assert_eq!(s, result)
}
#[test]
fn change_group_id_unchecked() {
test_change_group_id_unchecked("nothing", "R02", "nothing");
test_change_group_id_unchecked("[[Hopefully Not Hidden]]", "R02", "[[R02]]");
test_change_group_id_unchecked("colo[[[u]]]r", "u", "colo[[u]]]r");
test_change_group_id_unchecked(
"Before[[[[Anything]]]]After",
"Sunrise",
"Before[[[[Anything]]]]After", );
test_change_group_id_unchecked(
"Obsidian Internal Link [\\[Anything]\\]",
".....",
"Obsidian Internal Link [\\[Anything]\\]",
);
test_change_group_id_unchecked(
"Front[\\[:[\\[Center]\\]:]\\]Back",
".....",
"Front[\\[:[\\[Center]\\]:]\\]Back",
);
test_change_group_id_unchecked(
"multi_groupid_Leg_[\\[L04]\\]_Claw_[[L01]]",
"R09",
"multi_groupid_Leg_[\\[L04]\\]_Claw_[[R09]]",
);
test_change_group_id_unchecked(
"Front[\\[:[[Center]]:]\\]Back",
"Middle",
"Front[\\[:[[Middle]]:]\\]Back",
);
test_change_group_id_unchecked(
"multi_groupid_Leg_[\\[L04]\\]_Claw_[[L01]]",
"[[R08]]",
"multi_groupid_Leg_[\\[L04]\\]_Claw_[[[[R08]]]]",
);
test_change_group_id_unchecked(
"Front[\\[:[[Center]]:]\\]Back",
"",
"Front[\\[:[[]]:]\\]Back",
);
}
fn test_change_group_id(
s: impl Into<String>,
new_group_id: &str,
func_result: Result<(), GroupIDError>,
new_identifier: &str,
) {
let mut s: String = s.into();
assert_eq!(s.change_group_id(new_group_id), func_result);
assert_eq!(s, new_identifier);
}
#[test]
fn change_group_id() {
test_change_group_id("nothing", "R02", Ok(()), "nothing");
test_change_group_id("[[Hopefully Not Hidden]]", "R02", Ok(()), "[[R02]]");
test_change_group_id("colo[[[u]]]r", "u", Ok(()), "colo[[u]]]r");
test_change_group_id(
"Before[[[[Anything]]]]After",
"Sunrise",
Ok(()),
"Before[[[[Anything]]]]After", );
test_change_group_id(
"Obsidian Internal Link [\\[Anything]\\]",
".....",
Ok(()),
"Obsidian Internal Link [\\[Anything]\\]",
);
test_change_group_id(
"Front[\\[:[\\[Center]\\]:]\\]Back",
".....",
Ok(()),
"Front[\\[:[\\[Center]\\]:]\\]Back",
);
test_change_group_id(
"multi_groupid_Leg_[\\[L04]\\]_Claw_[[L01]]",
"R09",
Ok(()),
"multi_groupid_Leg_[\\[L04]\\]_Claw_[[R09]]",
);
test_change_group_id(
"Front[\\[:[[Center]]:]\\]Back",
"Middle",
Ok(()),
"Front[\\[:[[Middle]]:]\\]Back",
);
test_change_group_id(
"multi_groupid_Leg_[\\[L04]\\]_Claw_[[L01]]",
"[[R08]]",
Err(GroupIDError {
invalid_group_id: "[[R08]]".into(),
kind: GroupIDErrorKind::ContainsOpen,
}),
"multi_groupid_Leg_[\\[L04]\\]_Claw_[[L01]]",
);
test_change_group_id(
"Front[\\[:[[Center]]:]\\]Back",
"",
Err(GroupIDError {
invalid_group_id: String::new(),
kind: GroupIDErrorKind::Empty,
}),
"Front[\\[:[[Center]]:]\\]Back",
);
}
fn test_apply_group_id(s: impl Into<String>, result: &str) {
let mut s: String = s.into();
s.apply_group_id();
assert_eq!(s, result);
}
#[test]
fn apply_group_id() {
test_apply_group_id("nothing", "nothing");
test_apply_group_id("[[Hopefully Not Hidden]]", "Hopefully Not Hidden");
test_apply_group_id("colo[[[u]]]r", "colo[u]r");
test_apply_group_id(
"Before[[[[Anything]]]]After",
"Before[[[[Anything]]]]After",
);
test_apply_group_id(
"Obsidian Internal Link [\\[Anything]\\]",
"Obsidian Internal Link [[Anything]]",
);
test_apply_group_id(
"Front[\\[:[\\[Center]\\]:]\\]Back",
"Front[[:[[Center]]:]]Back",
);
test_apply_group_id(
"multi_groupid_Leg_[\\[L04]\\]_Claw_[[L01]]",
"multi_groupid_Leg_[[L04]]_Claw_L01",
);
test_apply_group_id("Front[\\[:[[Center]]:]\\]Back", "Front[[:Center:]]Back");
test_apply_group_id(
"multi_groupid_Leg_[\\[L04]\\]]_Claw_[[L01]]",
"multi_groupid_Leg_[\\[L04]\\]]_Claw_[[L01]]",
);
}
}
}