manabrew_engine/keyword/
keyword_collection.rs1use std::collections::HashMap;
6
7use serde::{Deserialize, Deserializer, Serialize, Serializer};
8
9use super::keyword_instance::{Keyword, KeywordInstanceData};
10
11#[derive(Debug, Clone, Default)]
16pub struct KeywordCollection {
17 map: HashMap<Keyword, Vec<KeywordInstanceData>>,
19}
20
21impl Serialize for KeywordCollection {
22 fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
23 self.as_string_list().serialize(serializer)
24 }
25}
26
27impl<'de> Deserialize<'de> for KeywordCollection {
28 fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
29 let strings: Vec<String> = Vec::deserialize(deserializer)?;
30 let mut coll = KeywordCollection::new();
31 for s in &strings {
32 coll.add(s);
33 }
34 Ok(coll)
35 }
36}
37
38impl KeywordCollection {
39 pub fn new() -> Self {
41 Self {
42 map: HashMap::new(),
43 }
44 }
45
46 pub fn from_strings(strings: &[String]) -> Self {
48 let mut coll = Self::new();
49 for s in strings {
50 coll.add(s);
51 }
52 coll
53 }
54
55 pub fn contains_keyword(&self, keyword: Keyword) -> bool {
57 self.map.contains_key(&keyword)
58 }
59
60 pub fn is_empty(&self) -> bool {
62 self.map.values().all(|v| v.is_empty())
63 }
64
65 pub fn size(&self) -> usize {
67 self.map.values().map(|v| v.len()).sum()
68 }
69
70 pub fn get_amount(&self, keyword: Keyword) -> i32 {
72 self.map
73 .get(&keyword)
74 .map(|instances| instances.len() as i32)
75 .unwrap_or(0)
76 }
77
78 pub fn insert(&mut self, inst: KeywordInstanceData) -> bool {
80 let keyword = inst.keyword;
81 let list = self.map.entry(keyword).or_default();
82 if keyword.is_multiple_redundant() {
83 for existing in list.iter() {
85 if existing.original == inst.original {
86 return false;
87 }
88 }
89 }
90 list.push(inst);
91 true
92 }
93
94 pub fn add(&mut self, k: &str) -> bool {
96 let (keyword, _details) = parse_keyword_string(k);
97 let inst = KeywordInstanceData::new(keyword, k.to_string());
98 self.insert(inst)
99 }
100
101 pub fn add_all<'a>(&mut self, keywords: impl IntoIterator<Item = &'a str>) {
103 for k in keywords {
104 self.add(k);
105 }
106 }
107
108 pub fn remove(&mut self, keyword: &str) -> bool {
110 let mut result = false;
111 for list in self.map.values_mut() {
112 let before = list.len();
113 list.retain(|inst| !inst.original.starts_with(keyword));
114 if list.len() != before {
115 result = true;
116 }
117 }
118 result
119 }
120
121 pub fn remove_all(&mut self, keyword: Keyword) -> bool {
123 self.map
124 .remove(&keyword)
125 .map(|v| !v.is_empty())
126 .unwrap_or(false)
127 }
128
129 pub fn remove_strings<'a>(&mut self, keywords: impl IntoIterator<Item = &'a str>) -> bool {
131 let mut result = false;
132 for k in keywords {
133 if self.remove(k) {
134 result = true;
135 }
136 }
137 result
138 }
139
140 pub fn clear(&mut self) {
142 self.map.clear();
143 }
144
145 pub fn contains_string(&self, keyword: &str) -> bool {
147 self.map
148 .values()
149 .any(|list| list.iter().any(|inst| inst.original == keyword))
150 }
151
152 pub fn contains_string_ignore_case(&self, keyword: &str) -> bool {
154 self.map.values().any(|list| {
155 list.iter()
156 .any(|inst| inst.original.eq_ignore_ascii_case(keyword))
157 })
158 }
159
160 pub fn any_starts_with(&self, prefix: &str) -> bool {
162 self.map
163 .values()
164 .any(|list| list.iter().any(|inst| inst.original.starts_with(prefix)))
165 }
166
167 pub fn any_starts_with_ignore_case(&self, prefix: &str) -> bool {
169 let lower = prefix.to_lowercase();
170 self.map.values().any(|list| {
171 list.iter()
172 .any(|inst| inst.original.to_lowercase().starts_with(&lower))
173 })
174 }
175
176 pub fn find_with_prefix(&self, prefix: &str) -> Option<&str> {
178 for list in self.map.values() {
179 for inst in list {
180 if inst.original.starts_with(prefix) {
181 return Some(&inst.original);
182 }
183 }
184 }
185 None
186 }
187
188 pub fn iter_strings(&self) -> impl Iterator<Item = &str> {
190 self.map
191 .values()
192 .flat_map(|v| v.iter().map(|inst| inst.original.as_str()))
193 }
194
195 pub fn retain<F: Fn(&str) -> bool>(&mut self, f: F) {
197 for list in self.map.values_mut() {
198 list.retain(|inst| f(&inst.original));
199 }
200 self.map.retain(|_, v| !v.is_empty());
202 }
203
204 pub fn extend(&mut self, other: impl IntoIterator<Item = String>) {
206 for s in other {
207 self.add(&s);
208 }
209 }
210
211 pub fn get_values(&self) -> Vec<&KeywordInstanceData> {
213 self.map.values().flat_map(|v| v.iter()).collect()
214 }
215
216 pub fn get_values_for(&self, keyword: Keyword) -> Vec<&KeywordInstanceData> {
218 self.map
219 .get(&keyword)
220 .map(|v| v.iter().collect())
221 .unwrap_or_default()
222 }
223
224 pub fn contains(&self, keyword: &str) -> bool {
227 self.map
228 .values()
229 .any(|list| list.iter().any(|inst| inst.original.starts_with(keyword)))
230 }
231
232 pub fn insert_all(&mut self, keywords: &[KeywordInstanceData]) {
235 for inst in keywords {
236 self.insert(inst.clone());
237 }
238 }
239
240 pub fn remove_instances(&mut self, keyword: &str) {
243 for list in self.map.values_mut() {
244 list.retain(|inst| inst.original != keyword);
245 }
246 self.map.retain(|_, v| !v.is_empty());
247 }
248
249 pub fn apply_changes(&mut self, additions: &[String], removals: &[String]) {
252 for r in removals {
253 self.remove(r);
254 }
255 for a in additions {
256 self.add(a);
257 }
258 }
259
260 pub fn iterator(&self) -> impl Iterator<Item = &str> {
263 self.iter_strings()
264 }
265
266 pub fn as_string_list(&self) -> Vec<String> {
268 self.map
269 .values()
270 .flat_map(|v| v.iter().map(|inst| inst.original.clone()))
271 .collect()
272 }
273}
274
275pub(crate) fn parse_keyword_string(k: &str) -> (Keyword, String) {
278 if k.contains(':') {
279 let parts: Vec<&str> = k.splitn(2, ':').collect();
280 let keyword = Keyword::smart_value_of(parts[0]);
281 let mut details = parts[1].to_string();
282 if let Some(idx) = details.find(":Flavor ") {
284 details.truncate(idx);
285 }
286 (keyword, details)
287 } else if k.contains(' ') {
288 let keyword = Keyword::smart_value_of(k);
290 if keyword != Keyword::Undefined {
291 return (keyword, String::new());
292 }
293 let parts: Vec<&str> = k.splitn(2, ' ').collect();
295 let keyword = Keyword::smart_value_of(parts[0]);
296 if keyword != Keyword::Undefined {
297 (keyword, parts[1].to_string())
298 } else {
299 (Keyword::Undefined, k.to_string())
300 }
301 } else {
302 let keyword = Keyword::smart_value_of(k);
303 (keyword, String::new())
304 }
305}
306
307#[cfg(test)]
308mod tests {
309 use super::*;
310
311 #[test]
312 fn test_add_simple_keyword() {
313 let mut coll = KeywordCollection::new();
314 assert!(coll.add("Flying"));
315 assert!(coll.contains_keyword(Keyword::Flying));
316 assert!(coll.contains_string("Flying"));
317 }
318
319 #[test]
320 fn test_redundant_keyword_not_added_twice() {
321 let mut coll = KeywordCollection::new();
322 assert!(coll.add("Flying"));
323 assert!(!coll.add("Flying"));
324 assert_eq!(coll.size(), 1);
325 }
326
327 #[test]
328 fn test_keyword_with_cost() {
329 let mut coll = KeywordCollection::new();
330 assert!(coll.add("Kicker:1 R"));
331 assert!(coll.contains_keyword(Keyword::Kicker));
332 }
333
334 #[test]
335 fn test_remove_keyword() {
336 let mut coll = KeywordCollection::new();
337 coll.add("Flying");
338 coll.add("Haste");
339 assert!(coll.remove("Flying"));
340 assert!(!coll.contains_string("Flying"));
341 assert!(coll.contains_string("Haste"));
342 }
343
344 #[test]
345 fn test_clear() {
346 let mut coll = KeywordCollection::new();
347 coll.add("Flying");
348 coll.add("Haste");
349 coll.clear();
350 assert!(coll.is_empty());
351 }
352
353 #[test]
354 fn test_contains_string_ignore_case() {
355 let mut coll = KeywordCollection::new();
356 coll.add("Flying");
357 assert!(coll.contains_string_ignore_case("flying"));
358 assert!(coll.contains_string_ignore_case("FLYING"));
359 assert!(!coll.contains_string_ignore_case("Haste"));
360 }
361
362 #[test]
363 fn test_any_starts_with() {
364 let mut coll = KeywordCollection::new();
365 coll.add("Protection from red");
366 assert!(coll.any_starts_with("Protection from "));
367 assert!(!coll.any_starts_with("Flying"));
368 }
369
370 #[test]
371 fn test_iter_strings() {
372 let mut coll = KeywordCollection::new();
373 coll.add("Flying");
374 coll.add("Haste");
375 let strings: Vec<&str> = coll.iter_strings().collect();
376 assert_eq!(strings.len(), 2);
377 assert!(strings.contains(&"Flying"));
378 assert!(strings.contains(&"Haste"));
379 }
380
381 #[test]
382 fn test_retain() {
383 let mut coll = KeywordCollection::new();
384 coll.add("Flying");
385 coll.add("Haste");
386 coll.add("Menace");
387 coll.retain(|k| k != "Menace");
388 assert!(coll.contains_string("Flying"));
389 assert!(coll.contains_string("Haste"));
390 assert!(!coll.contains_string("Menace"));
391 }
392
393 #[test]
394 fn test_serde_roundtrip() {
395 let mut coll = KeywordCollection::new();
396 coll.add("Flying");
397 coll.add("Haste");
398 let json = serde_json::to_string(&coll).unwrap();
399 let deserialized: KeywordCollection = serde_json::from_str(&json).unwrap();
400 assert!(deserialized.contains_keyword(Keyword::Flying));
401 assert!(deserialized.contains_keyword(Keyword::Haste));
402 }
403}