//! Card subtypes
//!
//! Impl note: Make sure both the canonical representation [`Display`](Subtype#impl-Display-for-Subtype) and
//! the exact representation [`Debug`](Subtype#impl-Debug-for-Subtype) are accessible, as membership relations
//! (both quasi and not)
//! use the exact to determine the relation, but canonical gets the general point across.
//!
//! # Examples
//!
//! ```
//! # use magister::card::Subtype;
//! let subtype = Subtype::new("Mad:Dot");
//! assert!(subtype.is_quasimember("mad"));
//! assert!(!subtype.is_quasimember("mad-dot"));
//! assert!(subtype.is_quasimember("mad:dot"));
//! assert_eq!(&subtype.to_string(), "Mad-Dot");
//! assert_eq!(&format!("{subtype:?}"), "mad:dot");
//! ```
use heck::ToTrainCase;
use regex::Regex;
use serde::de::Visitor;
use std::{
collections::{HashMap, HashSet},
fmt,
sync::{Arc, Mutex, OnceLock},
};
#[derive(Clone)]
pub struct Subtype(HashSet<Arc<str>>, Arc<str>);
impl PartialEq for Subtype {
fn eq(&self, other: &Self) -> bool {
self.0 == other.0
}
}
impl Eq for Subtype {}
impl serde::Serialize for Subtype {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
serializer.serialize_str(&format!("{self:?}"))
}
}
pub(crate) struct SubtypeVisitor;
impl<'de> Visitor<'de> for SubtypeVisitor {
type Value = Box<str>;
fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
formatter.write_str("a string of space separated subtypes")
}
fn visit_str<E>(self, v: &str) -> Result<Self::Value, E>
where
E: serde::de::Error,
{
Ok(Box::from(v))
}
}
impl<'de> serde::Deserialize<'de> for Subtype {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: serde::Deserializer<'de>,
{
deserializer
.deserialize_str(SubtypeVisitor)
.map(Subtype::new)
}
}
impl fmt::Display for Subtype {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(
f,
"{}",
self.1
.split_whitespace()
.map(|s| s.to_train_case())
.collect::<Vec<_>>()
.join(" ")
)
}
}
impl fmt::Debug for Subtype {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
if f.alternate() {
f.debug_tuple("Subtype")
.field(&self.0)
.field(&self.1)
.finish()
} else {
write!(f, "{}", self.1)
}
}
}
impl Subtype {
pub fn new<S: AsRef<str>>(subtype: S) -> Self {
static REMOVE_SEPARATORS: OnceLock<Regex> = OnceLock::new();
let remove_seps =
REMOVE_SEPARATORS.get_or_init(|| Regex::new(r"(\A|\s)[-+/\\:](\z|\s)").unwrap());
let subtype = subtype.as_ref().trim().to_lowercase();
let subtype = remove_seps.replace_all(&subtype, " ");
Self(
subtype
.split_whitespace()
.filter(|s| !["-", "+", "/", r"\", ":"].contains(s))
.map(Arc::from)
.collect(),
Arc::from(subtype),
)
}
/// Provide an [`ExactSizeIterator`] of all subtypes
pub fn subtypes(&self) -> impl ExactSizeIterator<Item = &str> {
self.0.iter().map(|s| s.as_ref())
}
/// Check if this subtype is a member of a given subtype group
pub fn is_member<S: AsRef<str>>(&self, member: S) -> bool {
let member = member.as_ref().to_lowercase();
self.0.iter().any(|sm| sm.as_ref() == member)
}
/// Check if this subtype is a quasimember of a given subtype group
///
/// Quasimembership is defined for strings separated by `[-+/\:]`
pub fn is_quasimember<S: AsRef<str>>(&self, quasimember: S) -> bool {
pub(crate) static SUBTYPE_REGEXES: OnceLock<Mutex<HashMap<Box<str>, Regex>>> =
OnceLock::new();
let mut subtype_regex_map = SUBTYPE_REGEXES
.get_or_init(|| Mutex::new(HashMap::new()))
.lock()
.unwrap();
let subtype_regex = subtype_regex_map
.entry(Box::from(quasimember.as_ref().to_lowercase()))
.or_insert_with(|| {
Regex::new(&format!(
r"(\A|[-+/\\:]){}(\z|[-+/\\:])",
regex::escape(&quasimember.as_ref().to_lowercase())
))
.unwrap()
});
self.0.iter().any(|sm| subtype_regex.is_match(sm))
}
}
#[cfg(test)]
mod tests {
use super::Subtype;
#[test]
fn membership_will_only_match_whole_subtypes() {
let subtype = Subtype::new("Mad Relic");
let subtype2 = Subtype::new("Mad-Devouring Dragon");
assert!(subtype.is_member("Mad"));
assert!(!subtype2.is_member("Mad"));
}
#[test]
fn quasimembership_matches_words_in_subtypes() {
let subtype = Subtype::new("Mad Relic");
let subtype2 = Subtype::new("Mad-Devouring Dragon");
assert!(subtype.is_quasimember("Mad"));
assert!(subtype2.is_quasimember("Mad"));
assert!(!subtype2.is_quasimember("evo"));
}
#[test]
fn membership_and_quasimembership_are_caseinsensitive() {
let subtype = Subtype::new("Magic-Spellcaster Ruler");
assert!(subtype.is_member("MAGIC-SPELLCASTER"));
assert!(subtype.is_quasimember("SPELLcAsTeR"));
let subtype2 = Subtype::new("MAGIC-SPELLcaster RULer");
assert_eq!(subtype, subtype2);
}
#[test]
fn whitespace_doesnt_affect_subtype() {
let subtype = Subtype::new("Magic Ruler");
let subtype2 = Subtype::new("Magic \n\t\t Ruler");
assert_eq!(subtype, subtype2);
}
#[test]
fn valid_unicode_subtypes() {
let subtype = Subtype::new("Hailstone (-_-/ 😻-Ruler");
assert!(subtype.is_member("(-_-/"));
assert!(subtype.is_member("😻-ruler"));
assert!(subtype.is_quasimember("(-_-/"));
assert!(subtype.is_quasimember("("));
assert!(subtype.is_quasimember("_"));
assert!(subtype.is_quasimember("/"));
assert!(subtype.is_quasimember("😻"));
assert!(subtype.is_quasimember("Ruler"));
}
#[test]
fn test_serde() {
let subtype_a = Subtype::new("Mad:Dot / Dasher");
let subtype_b = Subtype::new("Mad-dot - Dasher");
let subtype_c = Subtype::new("Mad:Dot - Dasher");
assert_eq!(subtype_a, subtype_c);
assert_ne!(subtype_a, subtype_b);
assert_ne!(subtype_c, subtype_b);
insta::assert_ron_snapshot!([subtype_a, subtype_b, subtype_c]);
}
}