1use crate::{Tag, TagFromStringError};
8#[cfg(feature = "serde")]
9use serde::{de, Deserialize, Deserializer};
10use std::borrow::Borrow;
11use std::collections::HashSet;
12use std::error::Error;
13use std::fmt::{Display, Formatter};
14use std::hash::{Hash, Hasher};
15use std::iter::FromIterator;
16use std::ops::Deref;
17use std::str::FromStr;
18
19#[derive(Debug, Default, Clone, Eq, PartialEq)]
36pub struct TagUnion(HashSet<Tag>);
37
38impl TagUnion {
39 pub fn matches_set(&self, values: &HashSet<Tag>) -> bool {
67 self.0.is_subset(values)
68 }
69
70 pub fn insert(&mut self, tag: Tag) -> bool {
76 self.0.insert(tag)
77 }
78
79 pub fn remove<T: Borrow<Tag>>(&mut self, tag: T) -> bool {
85 self.0.remove(tag.borrow())
86 }
87
88 pub fn contains<T: Borrow<Tag>>(&self, tag: &T) -> bool {
93 self.0.contains(tag.borrow())
94 }
95
96 pub fn from_str<S: AsRef<str>>(value: S) -> Result<TagUnion, TagUnionFromStringError> {
98 let value = value.as_ref();
99 if value.is_empty() {
100 return Ok(TagUnion::default());
101 }
102
103 let parts = value.split('+');
104 let names: HashSet<String> = parts
105 .filter(|&c| !c.contains('+'))
106 .filter(|&c| !c.is_empty())
107 .map(|c| c.into())
108 .collect();
109
110 if names.is_empty() {
111 return Ok(TagUnion::default());
112 }
113
114 let mut tags = HashSet::new();
115 for name in names.into_iter() {
116 tags.insert(Tag::from_str(&name)?);
117 }
118
119 Ok(Self(tags))
120 }
121}
122
123pub trait MatchesAnyTagUnion {
125 fn matches_set(&self, values: &HashSet<Tag>) -> bool;
153}
154
155impl MatchesAnyTagUnion for Vec<TagUnion> {
156 fn matches_set(&self, values: &HashSet<Tag>) -> bool {
157 self.iter().any(|s| s.matches_set(&values))
158 }
159}
160
161impl Deref for TagUnion {
162 type Target = HashSet<Tag>;
163
164 fn deref(&self) -> &Self::Target {
165 &self.0
166 }
167}
168
169impl Hash for TagUnion {
170 fn hash<H: Hasher>(&self, state: &mut H) {
171 let mut vec = Vec::from_iter(self.0.iter());
172 vec.sort();
173 for tag in vec {
174 tag.hash(state);
175 }
176 }
177}
178
179impl FromIterator<Tag> for TagUnion {
180 fn from_iter<T: IntoIterator<Item = Tag>>(iter: T) -> Self {
181 Self(iter.into_iter().collect())
182 }
183}
184
185impl FromStr for TagUnion {
186 type Err = TagUnionFromStringError;
187
188 fn from_str(value: &str) -> Result<Self, Self::Err> {
189 TagUnion::from_str(value)
190 }
191}
192
193#[cfg_attr(docsrs, doc(cfg(feature = "serde")))]
194#[cfg(feature = "serde")]
195impl<'de> Deserialize<'de> for TagUnion {
196 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
197 where
198 D: Deserializer<'de>,
199 {
200 let input = String::deserialize(deserializer)?;
201 match TagUnion::from_str(&input) {
202 Ok(tags) => Ok(tags),
203 Err(e) => Err(de::Error::custom(e)),
204 }
205 }
206}
207
208#[derive(Debug, Eq, PartialEq)]
209pub enum TagUnionFromStringError {
210 InvalidTag(TagFromStringError),
211}
212
213impl Display for TagUnionFromStringError {
214 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
215 match self {
216 TagUnionFromStringError::InvalidTag(e) => write!(f, "Invalid tag: {e}"),
217 }
218 }
219}
220
221impl From<TagFromStringError> for TagUnionFromStringError {
222 fn from(value: TagFromStringError) -> Self {
223 Self::InvalidTag(value)
224 }
225}
226
227impl Error for TagUnionFromStringError {}
228
229#[cfg(test)]
230mod tests {
231 use super::*;
232
233 #[test]
234 fn test_empty() {
235 let tags: TagUnion = TagUnion::from_str("").unwrap();
236 assert!(tags.is_empty());
237 }
238
239 #[test]
240 fn test_add_remove() {
241 let mut tags = TagUnion::from_str(r#"foo"#).unwrap();
242 assert!(tags.contains(&Tag::new("foo")));
243 assert_eq!(tags.len(), 1);
244
245 tags.insert(Tag::new("bar"));
246 assert_eq!(tags.len(), 2);
247 tags.contains(&Tag::new("bar"));
248
249 tags.remove(&Tag::new("foo"));
250 assert!(!tags.contains(&Tag::new("foo")));
251 assert_eq!(tags.len(), 1);
252
253 tags.remove(&Tag::new("bar"));
254 assert_eq!(tags.len(), 0);
255 assert!(tags.is_empty());
256 }
257
258 #[test]
259 #[cfg(feature = "serde")]
260 fn test_trivial() {
261 let tags: TagUnion = serde_json::from_str(r#""foo""#).unwrap();
262 assert!(tags.contains(&Tag::new("foo")));
263 }
264
265 #[test]
266 #[cfg(feature = "serde")]
267 fn test_complex() {
268 let tags: TagUnion = serde_json::from_str(r#""foo+bar+++baz++""#).unwrap();
269 assert_eq!(tags.len(), 3);
270 assert!(tags.contains(&Tag::new("foo")));
271 assert!(tags.contains(&Tag::new("bar")));
272 assert!(tags.contains(&Tag::new("baz")));
273 }
274
275 #[test]
276 fn test_invalid() {
277 let tags = TagUnion::from_str(r#"foo+#baz"#);
278 assert_eq!(
279 tags,
280 Err(TagUnionFromStringError::InvalidTag(
281 crate::TagFromStringError::MustStartAlphabetic('#')
282 ))
283 );
284 }
285
286 #[test]
287 fn test_matches() {
288 let selections = vec![
289 TagUnion::from_str("foo+bar").unwrap(),
290 TagUnion::from_str("baz").unwrap(),
291 ];
292
293 assert!(selections.matches_set(&HashSet::from_iter([
295 Tag::new("foo"),
296 Tag::new("bar"),
297 Tag::new("baz"),
298 ])));
299
300 assert!(selections.matches_set(&HashSet::from_iter([Tag::new("baz"),])));
302
303 assert!(selections.matches_set(&HashSet::from_iter([Tag::new("foo"), Tag::new("bar"),])));
305
306 assert!(selections.matches_set(&HashSet::from_iter([Tag::new("foo"), Tag::new("baz"),])));
308
309 assert!(!selections.matches_set(&HashSet::from_iter([Tag::new("foo"), Tag::new("bang"),])));
311 }
312}