use std::collections::BTreeSet;
#[derive(Debug, Clone, Copy, PartialEq, Eq, thiserror::Error)]
pub enum AudienceSetError {
#[error("an audience set must contain at least one audience")]
Empty,
#[error("an audience must not be blank")]
BlankAudience,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct AudienceSet(BTreeSet<String>);
impl AudienceSet {
pub fn single(audience: impl Into<String>) -> Self {
let audience = audience.into();
Self(BTreeSet::from([audience]))
}
pub fn new<I, S>(audiences: I) -> Result<Self, AudienceSetError>
where
I: IntoIterator<Item = S>,
S: Into<String>,
{
let set: BTreeSet<String> = audiences.into_iter().map(Into::into).collect();
if set.is_empty() {
return Err(AudienceSetError::Empty);
}
if set.iter().any(|audience| audience.trim().is_empty()) {
return Err(AudienceSetError::BlankAudience);
}
Ok(Self(set))
}
pub fn accepts(&self, audience: &str) -> bool {
if audience.trim().is_empty() {
return false;
}
self.0.contains(audience)
}
pub fn iter(&self) -> impl Iterator<Item = &str> {
self.0.iter().map(String::as_str)
}
pub fn len(&self) -> usize {
self.0.len()
}
pub fn is_empty(&self) -> bool {
false
}
pub fn as_single(&self) -> Option<&str> {
if self.0.len() == 1 {
self.0.iter().next().map(String::as_str)
} else {
None
}
}
}
impl From<String> for AudienceSet {
fn from(audience: String) -> Self {
Self::single(audience)
}
}
impl From<&str> for AudienceSet {
fn from(audience: &str) -> Self {
Self::single(audience)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn a_single_audience_accepts_only_itself() {
let set = AudienceSet::single("deployment-31");
assert!(set.accepts("deployment-31"));
assert!(!set.accepts("deployment-32"));
assert_eq!(set.len(), 1);
assert_eq!(set.as_single(), Some("deployment-31"));
}
#[test]
fn a_set_accepts_every_member_and_nothing_else() {
let set = AudienceSet::new(["deployment-31", "deployment-32", "deployment-40"])
.expect("non-empty");
for member in ["deployment-31", "deployment-32", "deployment-40"] {
assert!(set.accepts(member), "{member} should be accepted");
}
assert!(!set.accepts("deployment-99"));
assert_eq!(
set.as_single(),
None,
"a set with several audiences has no single audience"
);
}
#[test]
fn membership_is_exact() {
let set = AudienceSet::single("deployment-3");
assert!(!set.accepts("deployment-31"));
assert!(!set.accepts("deployment"));
assert!(!set.accepts("DEPLOYMENT-3"));
assert!(!set.accepts(" deployment-3"));
assert!(!set.accepts(""));
}
#[test]
fn an_empty_or_blank_set_is_refused_at_construction() {
assert_eq!(
AudienceSet::new(Vec::<String>::new()).unwrap_err(),
AudienceSetError::Empty
);
assert_eq!(
AudienceSet::new([""]).unwrap_err(),
AudienceSetError::BlankAudience
);
assert_eq!(
AudienceSet::new(["deployment-31", " "]).unwrap_err(),
AudienceSetError::BlankAudience
);
}
#[test]
fn a_blank_audience_never_authenticates_even_if_the_set_holds_one() {
let set = AudienceSet::single("");
assert!(!set.accepts(""));
assert!(!set.accepts(" "));
let set = AudienceSet::new(["deployment-31"]).expect("non-empty");
assert!(!set.accepts(""));
assert!(set.accepts("deployment-31"));
}
#[test]
fn duplicates_collapse() {
let set = AudienceSet::new(["deployment-31", "deployment-31"]).expect("non-empty");
assert_eq!(set.len(), 1);
}
}