use std::collections::BTreeSet;
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Default)]
pub enum Reach {
#[default]
Auto,
Remote,
Local,
}
impl Reach {
pub const fn as_str(self) -> &'static str {
match self {
Self::Auto => "auto",
Self::Remote => "remote",
Self::Local => "local",
}
}
pub const ALL: [Self; 3] = [Self::Auto, Self::Remote, Self::Local];
pub(crate) fn parse(text: &str) -> Option<Self> {
Self::ALL.into_iter().find(|reach| reach.as_str() == text)
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ReachPolicy {
default: Reach,
allowed: BTreeSet<Reach>,
}
impl Default for ReachPolicy {
fn default() -> Self {
Self {
default: Reach::Auto,
allowed: Reach::ALL.into_iter().collect(),
}
}
}
impl ReachPolicy {
pub fn fixed(reach: Reach) -> Self {
Self {
default: reach,
allowed: BTreeSet::from([reach]),
}
}
#[must_use]
pub fn with_default(mut self, reach: Reach) -> Self {
self.default = reach;
self.allowed.insert(reach);
self
}
pub fn default_reach(&self) -> Reach {
self.default
}
pub fn allows(&self, reach: Reach) -> bool {
self.allowed.contains(&reach)
}
pub fn allowed(&self) -> impl Iterator<Item = Reach> + '_ {
Reach::ALL
.into_iter()
.filter(|reach| self.allowed.contains(reach))
}
pub(crate) fn offers_a_choice(&self) -> bool {
self.allowed.len() > 1
}
pub(crate) fn decide(&self, said: Option<Reach>) -> Result<Reach, String> {
let reach = said.unwrap_or(self.default);
if self.allows(reach) {
return Ok(reach);
}
let allowed: Vec<String> = self
.allowed()
.map(|reach| format!("`{}`", reach.as_str()))
.collect();
Err(format!(
"`reach: {}` is not allowed here; {}",
reach.as_str(),
if allowed.len() == 1 {
format!(
"every `@<name>` reaches {} agents",
allowed[0].trim_matches('`')
)
} else {
format!("`reach` may be {}", allowed.join(" or "))
}
))
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn the_spellings_are_the_ones_rules_are_written_against() {
assert_eq!(Reach::Auto.as_str(), "auto");
assert_eq!(Reach::Remote.as_str(), "remote");
assert_eq!(Reach::Local.as_str(), "local");
for reach in Reach::ALL {
assert_eq!(Reach::parse(reach.as_str()), Some(reach));
}
assert_eq!(Reach::parse("anywhere"), None);
}
#[test]
fn the_default_policy_allows_everything_and_reads_silence_as_auto() {
let policy = ReachPolicy::default();
assert_eq!(policy.decide(None), Ok(Reach::Auto));
for reach in Reach::ALL {
assert_eq!(policy.decide(Some(reach)), Ok(reach));
}
assert!(policy.offers_a_choice());
}
#[test]
fn a_fixed_policy_refuses_the_rest_and_names_what_every_name_reaches() {
let policy = ReachPolicy::fixed(Reach::Remote);
assert_eq!(policy.decide(None), Ok(Reach::Remote));
let refusal = policy.decide(Some(Reach::Local)).expect_err("not allowed");
assert!(
refusal.contains("`reach: local` is not allowed"),
"{refusal}"
);
assert!(refusal.contains("reaches remote agents"), "{refusal}");
assert!(!policy.offers_a_choice());
}
#[test]
fn a_default_is_always_allowed() {
let policy = ReachPolicy::fixed(Reach::Remote).with_default(Reach::Auto);
assert_eq!(policy.decide(None), Ok(Reach::Auto));
assert_eq!(
policy.allowed().collect::<Vec<_>>(),
[Reach::Auto, Reach::Remote]
);
let refusal = policy.decide(Some(Reach::Local)).expect_err("not allowed");
assert!(refusal.contains("`auto` or `remote`"), "{refusal}");
}
}