use std::collections::HashMap;
use serde::{Deserialize, Deserializer, Serialize, Serializer};
use super::keyword_instance::{Keyword, KeywordInstanceData};
#[derive(Debug, Clone, Default)]
pub struct KeywordCollection {
map: HashMap<Keyword, Vec<KeywordInstanceData>>,
}
impl Serialize for KeywordCollection {
fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
self.as_string_list().serialize(serializer)
}
}
impl<'de> Deserialize<'de> for KeywordCollection {
fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
let strings: Vec<String> = Vec::deserialize(deserializer)?;
let mut coll = KeywordCollection::new();
for s in &strings {
coll.add(s);
}
Ok(coll)
}
}
impl KeywordCollection {
pub fn new() -> Self {
Self {
map: HashMap::new(),
}
}
pub fn from_strings(strings: &[String]) -> Self {
let mut coll = Self::new();
for s in strings {
coll.add(s);
}
coll
}
pub fn contains_keyword(&self, keyword: Keyword) -> bool {
self.map.contains_key(&keyword)
}
pub fn is_empty(&self) -> bool {
self.map.values().all(|v| v.is_empty())
}
pub fn size(&self) -> usize {
self.map.values().map(|v| v.len()).sum()
}
pub fn get_amount(&self, keyword: Keyword) -> i32 {
self.map
.get(&keyword)
.map(|instances| instances.len() as i32)
.unwrap_or(0)
}
pub fn insert(&mut self, inst: KeywordInstanceData) -> bool {
let keyword = inst.keyword;
let list = self.map.entry(keyword).or_default();
if keyword.is_multiple_redundant() {
for existing in list.iter() {
if existing.original == inst.original {
return false;
}
}
}
list.push(inst);
true
}
pub fn add(&mut self, k: &str) -> bool {
let (keyword, _details) = parse_keyword_string(k);
let inst = KeywordInstanceData::new(keyword, k.to_string());
self.insert(inst)
}
pub fn add_all<'a>(&mut self, keywords: impl IntoIterator<Item = &'a str>) {
for k in keywords {
self.add(k);
}
}
pub fn remove(&mut self, keyword: &str) -> bool {
let mut result = false;
for list in self.map.values_mut() {
let before = list.len();
list.retain(|inst| !inst.original.starts_with(keyword));
if list.len() != before {
result = true;
}
}
result
}
pub fn remove_all(&mut self, keyword: Keyword) -> bool {
self.map
.remove(&keyword)
.map(|v| !v.is_empty())
.unwrap_or(false)
}
pub fn remove_strings<'a>(&mut self, keywords: impl IntoIterator<Item = &'a str>) -> bool {
let mut result = false;
for k in keywords {
if self.remove(k) {
result = true;
}
}
result
}
pub fn clear(&mut self) {
self.map.clear();
}
pub fn contains_string(&self, keyword: &str) -> bool {
self.map
.values()
.any(|list| list.iter().any(|inst| inst.original == keyword))
}
pub fn contains_string_ignore_case(&self, keyword: &str) -> bool {
self.map.values().any(|list| {
list.iter()
.any(|inst| inst.original.eq_ignore_ascii_case(keyword))
})
}
pub fn any_starts_with(&self, prefix: &str) -> bool {
self.map
.values()
.any(|list| list.iter().any(|inst| inst.original.starts_with(prefix)))
}
pub fn any_starts_with_ignore_case(&self, prefix: &str) -> bool {
let lower = prefix.to_lowercase();
self.map.values().any(|list| {
list.iter()
.any(|inst| inst.original.to_lowercase().starts_with(&lower))
})
}
pub fn find_with_prefix(&self, prefix: &str) -> Option<&str> {
for list in self.map.values() {
for inst in list {
if inst.original.starts_with(prefix) {
return Some(&inst.original);
}
}
}
None
}
pub fn iter_strings(&self) -> impl Iterator<Item = &str> {
self.map
.values()
.flat_map(|v| v.iter().map(|inst| inst.original.as_str()))
}
pub fn retain<F: Fn(&str) -> bool>(&mut self, f: F) {
for list in self.map.values_mut() {
list.retain(|inst| f(&inst.original));
}
self.map.retain(|_, v| !v.is_empty());
}
pub fn extend(&mut self, other: impl IntoIterator<Item = String>) {
for s in other {
self.add(&s);
}
}
pub fn get_values(&self) -> Vec<&KeywordInstanceData> {
self.map.values().flat_map(|v| v.iter()).collect()
}
pub fn get_values_for(&self, keyword: Keyword) -> Vec<&KeywordInstanceData> {
self.map
.get(&keyword)
.map(|v| v.iter().collect())
.unwrap_or_default()
}
pub fn contains(&self, keyword: &str) -> bool {
self.map
.values()
.any(|list| list.iter().any(|inst| inst.original.starts_with(keyword)))
}
pub fn insert_all(&mut self, keywords: &[KeywordInstanceData]) {
for inst in keywords {
self.insert(inst.clone());
}
}
pub fn remove_instances(&mut self, keyword: &str) {
for list in self.map.values_mut() {
list.retain(|inst| inst.original != keyword);
}
self.map.retain(|_, v| !v.is_empty());
}
pub fn apply_changes(&mut self, additions: &[String], removals: &[String]) {
for r in removals {
self.remove(r);
}
for a in additions {
self.add(a);
}
}
pub fn iterator(&self) -> impl Iterator<Item = &str> {
self.iter_strings()
}
pub fn as_string_list(&self) -> Vec<String> {
self.map
.values()
.flat_map(|v| v.iter().map(|inst| inst.original.clone()))
.collect()
}
}
pub(crate) fn parse_keyword_string(k: &str) -> (Keyword, String) {
if k.contains(':') {
let parts: Vec<&str> = k.splitn(2, ':').collect();
let keyword = Keyword::smart_value_of(parts[0]);
let mut details = parts[1].to_string();
if let Some(idx) = details.find(":Flavor ") {
details.truncate(idx);
}
(keyword, details)
} else if k.contains(' ') {
let keyword = Keyword::smart_value_of(k);
if keyword != Keyword::Undefined {
return (keyword, String::new());
}
let parts: Vec<&str> = k.splitn(2, ' ').collect();
let keyword = Keyword::smart_value_of(parts[0]);
if keyword != Keyword::Undefined {
(keyword, parts[1].to_string())
} else {
(Keyword::Undefined, k.to_string())
}
} else {
let keyword = Keyword::smart_value_of(k);
(keyword, String::new())
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_add_simple_keyword() {
let mut coll = KeywordCollection::new();
assert!(coll.add("Flying"));
assert!(coll.contains_keyword(Keyword::Flying));
assert!(coll.contains_string("Flying"));
}
#[test]
fn test_redundant_keyword_not_added_twice() {
let mut coll = KeywordCollection::new();
assert!(coll.add("Flying"));
assert!(!coll.add("Flying"));
assert_eq!(coll.size(), 1);
}
#[test]
fn test_keyword_with_cost() {
let mut coll = KeywordCollection::new();
assert!(coll.add("Kicker:1 R"));
assert!(coll.contains_keyword(Keyword::Kicker));
}
#[test]
fn test_remove_keyword() {
let mut coll = KeywordCollection::new();
coll.add("Flying");
coll.add("Haste");
assert!(coll.remove("Flying"));
assert!(!coll.contains_string("Flying"));
assert!(coll.contains_string("Haste"));
}
#[test]
fn test_clear() {
let mut coll = KeywordCollection::new();
coll.add("Flying");
coll.add("Haste");
coll.clear();
assert!(coll.is_empty());
}
#[test]
fn test_contains_string_ignore_case() {
let mut coll = KeywordCollection::new();
coll.add("Flying");
assert!(coll.contains_string_ignore_case("flying"));
assert!(coll.contains_string_ignore_case("FLYING"));
assert!(!coll.contains_string_ignore_case("Haste"));
}
#[test]
fn test_any_starts_with() {
let mut coll = KeywordCollection::new();
coll.add("Protection from red");
assert!(coll.any_starts_with("Protection from "));
assert!(!coll.any_starts_with("Flying"));
}
#[test]
fn test_iter_strings() {
let mut coll = KeywordCollection::new();
coll.add("Flying");
coll.add("Haste");
let strings: Vec<&str> = coll.iter_strings().collect();
assert_eq!(strings.len(), 2);
assert!(strings.contains(&"Flying"));
assert!(strings.contains(&"Haste"));
}
#[test]
fn test_retain() {
let mut coll = KeywordCollection::new();
coll.add("Flying");
coll.add("Haste");
coll.add("Menace");
coll.retain(|k| k != "Menace");
assert!(coll.contains_string("Flying"));
assert!(coll.contains_string("Haste"));
assert!(!coll.contains_string("Menace"));
}
#[test]
fn test_serde_roundtrip() {
let mut coll = KeywordCollection::new();
coll.add("Flying");
coll.add("Haste");
let json = serde_json::to_string(&coll).unwrap();
let deserialized: KeywordCollection = serde_json::from_str(&json).unwrap();
assert!(deserialized.contains_keyword(Keyword::Flying));
assert!(deserialized.contains_keyword(Keyword::Haste));
}
}