use std::str::FromStr;
use super::VisibilityTier;
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum AudienceTier {
Internal,
Public,
Team(String),
Restricted(String),
}
#[derive(Debug, thiserror::Error)]
pub enum AudienceParseError {
#[error("audience must be one of: internal, public, team:<NAME>, restricted:<LABEL>")]
Unknown,
#[error("`team:` audience requires a non-empty NAME")]
MissingTeamName,
#[error("`restricted:` audience requires a non-empty LABEL")]
MissingRestrictedLabel,
}
impl FromStr for AudienceTier {
type Err = AudienceParseError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
let trimmed = s.trim();
if trimmed.eq_ignore_ascii_case("internal") {
return Ok(AudienceTier::Internal);
}
if trimmed.eq_ignore_ascii_case("public") {
return Ok(AudienceTier::Public);
}
if let Some(rest) = trimmed.strip_prefix("team:") {
let name = rest.trim();
if name.is_empty() {
return Err(AudienceParseError::MissingTeamName);
}
return Ok(AudienceTier::Team(name.to_string()));
}
if let Some(rest) = trimmed.strip_prefix("restricted:") {
let label = rest.trim();
if label.is_empty() {
return Err(AudienceParseError::MissingRestrictedLabel);
}
return Ok(AudienceTier::Restricted(label.to_string()));
}
Err(AudienceParseError::Unknown)
}
}
pub fn visible(visibility: &VisibilityTier, audience: &AudienceTier) -> bool {
match (visibility, audience) {
(VisibilityTier::Public, _) => true,
(VisibilityTier::Private { scope_label }, AudienceTier::Restricted(viewer)) => {
scope_label == viewer
}
(VisibilityTier::Private { .. }, _) => false,
(_, AudienceTier::Internal) => true,
(VisibilityTier::Internal, AudienceTier::Public)
| (VisibilityTier::Internal, AudienceTier::Restricted(_)) => false,
(VisibilityTier::Internal, AudienceTier::Team(_)) => true,
(VisibilityTier::TeamScoped { team_id }, AudienceTier::Team(name)) => team_id == name,
(VisibilityTier::TeamScoped { .. }, _) => false,
(VisibilityTier::Restricted { scope_label }, AudienceTier::Restricted(viewer_label)) => {
scope_label == viewer_label
}
(VisibilityTier::Restricted { .. }, _) => false,
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn public_is_universally_visible_and_team_matches_exact_id() {
assert!(visible(&VisibilityTier::Public, &AudienceTier::Public));
assert!(visible(
&VisibilityTier::TeamScoped {
team_id: "infra".into()
},
&AudienceTier::Team("infra".into())
));
assert!(!visible(
&VisibilityTier::TeamScoped {
team_id: "infra".into()
},
&AudienceTier::Team("design".into())
));
assert!(visible(
&VisibilityTier::Restricted {
scope_label: "legal".into()
},
&AudienceTier::Internal
));
assert!(!visible(
&VisibilityTier::Internal,
&AudienceTier::Restricted("legal".into())
));
}
#[test]
fn private_visible_only_to_matching_restricted_audience() {
let vis = VisibilityTier::Private {
scope_label: "sec-embargo".into(),
};
assert!(visible(
&vis,
&AudienceTier::Restricted("sec-embargo".into())
));
assert!(!visible(&vis, &AudienceTier::Restricted("legal".into())));
}
#[test]
fn private_is_hidden_even_from_the_all_seeing_internal_audience() {
let vis = VisibilityTier::Private {
scope_label: "sec-embargo".into(),
};
assert!(!visible(&vis, &AudienceTier::Internal));
assert!(!visible(&vis, &AudienceTier::Public));
assert!(!visible(&vis, &AudienceTier::Team("infra".into())));
}
#[test]
fn parse_audience_strings() {
assert_eq!(
"internal".parse::<AudienceTier>().unwrap(),
AudienceTier::Internal
);
assert_eq!(
"public".parse::<AudienceTier>().unwrap(),
AudienceTier::Public
);
assert_eq!(
"team:infra".parse::<AudienceTier>().unwrap(),
AudienceTier::Team("infra".into())
);
assert_eq!(
"restricted:legal".parse::<AudienceTier>().unwrap(),
AudienceTier::Restricted("legal".into())
);
assert!("team:".parse::<AudienceTier>().is_err());
assert!("nonsense".parse::<AudienceTier>().is_err());
}
}