1use std::collections::BTreeSet;
12
13#[derive(Debug, Clone, Copy, PartialEq, Eq, thiserror::Error)]
15pub enum AudienceSetError {
16 #[error("an audience set must contain at least one audience")]
21 Empty,
22 #[error("an audience must not be blank")]
25 BlankAudience,
26}
27
28#[derive(Debug, Clone, PartialEq, Eq)]
30pub struct AudienceSet(BTreeSet<String>);
31
32impl AudienceSet {
33 pub fn single(audience: impl Into<String>) -> Self {
35 let audience = audience.into();
36 Self(BTreeSet::from([audience]))
37 }
38
39 pub fn new<I, S>(audiences: I) -> Result<Self, AudienceSetError>
44 where
45 I: IntoIterator<Item = S>,
46 S: Into<String>,
47 {
48 let set: BTreeSet<String> = audiences.into_iter().map(Into::into).collect();
49 if set.is_empty() {
50 return Err(AudienceSetError::Empty);
51 }
52 if set.iter().any(|audience| audience.trim().is_empty()) {
53 return Err(AudienceSetError::BlankAudience);
54 }
55 Ok(Self(set))
56 }
57
58 pub fn accepts(&self, audience: &str) -> bool {
70 if audience.trim().is_empty() {
71 return false;
72 }
73 self.0.contains(audience)
74 }
75
76 pub fn iter(&self) -> impl Iterator<Item = &str> {
78 self.0.iter().map(String::as_str)
79 }
80
81 pub fn len(&self) -> usize {
83 self.0.len()
84 }
85
86 pub fn is_empty(&self) -> bool {
88 false
89 }
90
91 pub fn as_single(&self) -> Option<&str> {
93 if self.0.len() == 1 {
94 self.0.iter().next().map(String::as_str)
95 } else {
96 None
97 }
98 }
99}
100
101impl From<String> for AudienceSet {
102 fn from(audience: String) -> Self {
103 Self::single(audience)
104 }
105}
106
107impl From<&str> for AudienceSet {
108 fn from(audience: &str) -> Self {
109 Self::single(audience)
110 }
111}
112
113#[cfg(test)]
114mod tests {
115 use super::*;
116
117 #[test]
118 fn a_single_audience_accepts_only_itself() {
119 let set = AudienceSet::single("deployment-31");
120 assert!(set.accepts("deployment-31"));
121 assert!(!set.accepts("deployment-32"));
122 assert_eq!(set.len(), 1);
123 assert_eq!(set.as_single(), Some("deployment-31"));
124 }
125
126 #[test]
127 fn a_set_accepts_every_member_and_nothing_else() {
128 let set = AudienceSet::new(["deployment-31", "deployment-32", "deployment-40"])
129 .expect("non-empty");
130 for member in ["deployment-31", "deployment-32", "deployment-40"] {
131 assert!(set.accepts(member), "{member} should be accepted");
132 }
133 assert!(!set.accepts("deployment-99"));
134 assert_eq!(
135 set.as_single(),
136 None,
137 "a set with several audiences has no single audience"
138 );
139 }
140
141 #[test]
142 fn membership_is_exact() {
143 let set = AudienceSet::single("deployment-3");
146 assert!(!set.accepts("deployment-31"));
147 assert!(!set.accepts("deployment"));
148 assert!(!set.accepts("DEPLOYMENT-3"));
149 assert!(!set.accepts(" deployment-3"));
150 assert!(!set.accepts(""));
151 }
152
153 #[test]
154 fn an_empty_or_blank_set_is_refused_at_construction() {
155 assert_eq!(
156 AudienceSet::new(Vec::<String>::new()).unwrap_err(),
157 AudienceSetError::Empty
158 );
159 assert_eq!(
160 AudienceSet::new([""]).unwrap_err(),
161 AudienceSetError::BlankAudience
162 );
163 assert_eq!(
164 AudienceSet::new(["deployment-31", " "]).unwrap_err(),
165 AudienceSetError::BlankAudience
166 );
167 }
168
169 #[test]
170 fn a_blank_audience_never_authenticates_even_if_the_set_holds_one() {
171 let set = AudienceSet::single("");
175 assert!(!set.accepts(""));
176 assert!(!set.accepts(" "));
177
178 let set = AudienceSet::new(["deployment-31"]).expect("non-empty");
180 assert!(!set.accepts(""));
181 assert!(set.accepts("deployment-31"));
182 }
183
184 #[test]
185 fn duplicates_collapse() {
186 let set = AudienceSet::new(["deployment-31", "deployment-31"]).expect("non-empty");
187 assert_eq!(set.len(), 1);
188 }
189}