use alloc::borrow::ToOwned;
use alloc::boxed::Box;
use alloc::collections::BTreeSet;
use alloc::format;
use alloc::string::String;
use alloc::vec::Vec;
use core::fmt::{Debug, Display, Formatter, Result as FmtResult};
pub trait Has<K> {
fn has(&self, key: &K) -> bool;
}
impl<K: Ord> Has<K> for BTreeSet<K> {
fn has(&self, key: &K) -> bool {
self.contains(key)
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[cfg_attr(feature = "serde", serde(deny_unknown_fields))]
pub enum Requirement<K> {
Has(K),
All(Vec<Requirement<K>>),
Any(Vec<Requirement<K>>),
Not(Box<Requirement<K>>),
AtLeast {
count: usize,
requirements: Vec<Requirement<K>>,
},
}
impl<K> Requirement<K> {
#[must_use]
pub fn has(key: K) -> Self {
Self::Has(key)
}
#[must_use]
pub fn all(requirements: impl IntoIterator<Item = Self>) -> Self {
Self::All(requirements.into_iter().collect())
}
#[must_use]
pub fn any(requirements: impl IntoIterator<Item = Self>) -> Self {
Self::Any(requirements.into_iter().collect())
}
#[must_use]
#[expect(
clippy::should_implement_trait,
reason = "the name matches the Not variant it builds"
)]
pub fn not(requirement: Self) -> Self {
Self::Not(Box::new(requirement))
}
#[must_use]
pub fn at_least(count: usize, requirements: impl IntoIterator<Item = Self>) -> Self {
Self::AtLeast {
count,
requirements: requirements.into_iter().collect(),
}
}
#[must_use]
pub fn warning(&self) -> Option<String> {
match self {
Self::Has(_) => None,
Self::Any(items) if items.is_empty() => Some("An empty Any never holds.".to_owned()),
Self::All(items) | Self::Any(items) => first_child_warning(items),
Self::Not(inner) => inner
.warning()
.map(|warning| format!("Inner requirement: {warning}")),
Self::AtLeast {
count,
requirements,
} => {
if *count == 0 {
Some("At least 0 always holds.".to_owned())
} else if *count > requirements.len() {
Some(format!(
"Requires {count} of {}; it can never hold.",
requirements.len()
))
} else {
first_child_warning(requirements)
}
}
}
}
}
fn first_child_warning<K>(items: &[Requirement<K>]) -> Option<String> {
items.iter().enumerate().find_map(|(index, item)| {
item.warning()
.map(|warning| format!("Requirement {index}: {warning}"))
})
}
impl<K: Ord + Clone> Requirement<K> {
#[must_use]
pub fn required(&self) -> BTreeSet<K> {
self.edges().0
}
#[must_use]
pub fn optional(&self) -> BTreeSet<K> {
self.edges().1
}
pub(crate) fn edges(&self) -> (BTreeSet<K>, BTreeSet<K>) {
let mut sets = (BTreeSet::new(), BTreeSet::new());
self.collect_edges(&mut sets, false);
let (required, mut optional) = sets;
optional.retain(|key| !required.contains(key));
(required, optional)
}
fn collect_edges(&self, sets: &mut (BTreeSet<K>, BTreeSet<K>), in_choice: bool) {
match self {
Self::Has(key) => {
let side = if in_choice { &mut sets.1 } else { &mut sets.0 };
side.insert(key.clone());
}
Self::All(items) => {
for item in items {
item.collect_edges(sets, in_choice);
}
}
Self::Any(items)
| Self::AtLeast {
requirements: items,
..
} => {
for item in items {
item.collect_edges(sets, true);
}
}
Self::Not(_) => {}
}
}
}
impl<K> Requirement<K> {
#[must_use]
pub fn satisfies(&self, holder: &(impl Has<K> + ?Sized)) -> bool {
match self {
Self::Has(key) => holder.has(key),
Self::All(items) => items.iter().all(|item| item.satisfies(holder)),
Self::Any(items) => items.iter().any(|item| item.satisfies(holder)),
Self::Not(inner) => !inner.satisfies(holder),
Self::AtLeast {
count,
requirements,
} => {
let mut met = 0;
for (index, item) in requirements.iter().enumerate() {
let left = requirements.len() - index;
if met >= *count || met + left < *count {
break;
}
if item.satisfies(holder) {
met += 1;
}
}
met >= *count
}
}
}
#[must_use]
pub fn evaluate(&self, holder: &(impl Has<K> + ?Sized)) -> Evaluation<K>
where
K: Clone,
{
match self {
Self::Has(key) => Evaluation::Has {
key: key.clone(),
satisfied: holder.has(key),
},
Self::All(items) => {
let children = Self::evaluate_each(items, holder);
Evaluation::All {
satisfied: children.iter().all(Evaluation::satisfied),
children,
}
}
Self::Any(items) => {
let children = Self::evaluate_each(items, holder);
Evaluation::Any {
satisfied: children.iter().any(Evaluation::satisfied),
children,
}
}
Self::Not(inner) => {
let child = inner.evaluate(holder);
Evaluation::Not {
satisfied: !child.satisfied(),
child: Box::new(child),
}
}
Self::AtLeast {
count,
requirements,
} => {
let children = Self::evaluate_each(requirements, holder);
let met = children.iter().filter(|c| c.satisfied()).count();
Evaluation::AtLeast {
count: *count,
met,
satisfied: met >= *count,
children,
}
}
}
}
fn evaluate_each(items: &[Self], holder: &(impl Has<K> + ?Sized)) -> Vec<Evaluation<K>>
where
K: Clone,
{
items.iter().map(|item| item.evaluate(holder)).collect()
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[cfg_attr(feature = "serde", serde(deny_unknown_fields))]
pub enum Evaluation<K> {
Has {
key: K,
satisfied: bool,
},
All {
satisfied: bool,
children: Vec<Evaluation<K>>,
},
Any {
satisfied: bool,
children: Vec<Evaluation<K>>,
},
Not {
satisfied: bool,
child: Box<Evaluation<K>>,
},
AtLeast {
count: usize,
met: usize,
satisfied: bool,
children: Vec<Evaluation<K>>,
},
}
impl<K> Evaluation<K> {
#[must_use]
pub fn satisfied(&self) -> bool {
match self {
Self::Has { satisfied, .. }
| Self::All { satisfied, .. }
| Self::Any { satisfied, .. }
| Self::Not { satisfied, .. }
| Self::AtLeast { satisfied, .. } => *satisfied,
}
}
#[must_use]
pub fn children(&self) -> &[Evaluation<K>] {
match self {
Self::All { children, .. }
| Self::Any { children, .. }
| Self::AtLeast { children, .. } => children,
Self::Not { child, .. } => core::slice::from_ref(child),
Self::Has { .. } => &[],
}
}
}
impl<K: Clone + PartialEq> Evaluation<K> {
#[must_use]
pub fn missing(&self) -> Vec<K> {
let mut keys = Vec::new();
self.collect_missing(&mut keys);
keys
}
fn collect_missing(&self, keys: &mut Vec<K>) {
match self {
Self::Has {
key,
satisfied: false,
} => {
if !keys.contains(key) {
keys.push(key.clone());
}
}
Self::All { children, .. } => {
for child in children {
child.collect_missing(keys);
}
}
_ => {}
}
}
}
impl<K: Debug> Display for Evaluation<K> {
fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult {
self.write_at(f, 0)
}
}
impl<K: Debug> Evaluation<K> {
fn write_at(&self, f: &mut Formatter<'_>, depth: usize) -> FmtResult {
let mark = if self.satisfied() { '✓' } else { '✗' };
write!(f, "{:pad$}{mark} ", "", pad = depth * 2)?;
match self {
Self::Has { key, .. } => write!(f, "Has {key:?}")?,
Self::All { children, .. } => write!(f, "All of {}", children.len())?,
Self::Any { children, .. } => write!(f, "Any of {}", children.len())?,
Self::Not { .. } => write!(f, "Not")?,
Self::AtLeast {
count,
met,
children,
..
} => write!(f, "At least {count} of {}, {met} met", children.len())?,
}
for child in self.children() {
writeln!(f)?;
child.write_at(f, depth + 1)?;
}
Ok(())
}
}
#[cfg(test)]
mod tests {
use alloc::string::ToString;
use alloc::vec;
use super::*;
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)]
enum Tech {
A,
B,
C,
D,
}
use Tech::{A, B, C, D};
fn caps(keys: &[Tech]) -> BTreeSet<Tech> {
keys.iter().copied().collect()
}
#[test]
fn boolean_semantics() {
let cases: Vec<(&[Tech], Requirement<Tech>, bool)> = vec![
(&[A], Requirement::has(A), true),
(&[], Requirement::has(A), false),
(
&[A, B],
Requirement::all([Requirement::has(A), Requirement::has(B)]),
true,
),
(
&[A],
Requirement::all([Requirement::has(A), Requirement::has(B)]),
false,
),
(&[], Requirement::all([]), true),
(
&[A],
Requirement::any([Requirement::has(A), Requirement::has(B)]),
true,
),
(
&[C],
Requirement::any([Requirement::has(A), Requirement::has(B)]),
false,
),
(&[], Requirement::any([]), false),
(&[A], Requirement::not(Requirement::has(A)), false),
(&[], Requirement::not(Requirement::has(A)), true),
(
&[A],
Requirement::not(Requirement::all([Requirement::has(A), Requirement::has(B)])),
true,
),
(
&[A],
Requirement::at_least(
2,
[
Requirement::has(A),
Requirement::has(B),
Requirement::has(C),
],
),
false,
),
(
&[A, B],
Requirement::at_least(
2,
[
Requirement::has(A),
Requirement::has(B),
Requirement::has(C),
],
),
true,
),
(&[], Requirement::at_least(0, [Requirement::has(A)]), true),
(
&[A, B],
Requirement::at_least(5, [Requirement::has(A), Requirement::has(B)]),
false,
),
(
&[A, C],
Requirement::all([
Requirement::has(A),
Requirement::any([Requirement::has(B), Requirement::has(C)]),
]),
true,
),
(
&[B, C],
Requirement::all([
Requirement::has(A),
Requirement::any([Requirement::has(B), Requirement::has(C)]),
]),
false,
),
(
&[C, D],
Requirement::any([
Requirement::all([Requirement::has(A), Requirement::has(B)]),
Requirement::all([Requirement::has(C), Requirement::has(D)]),
]),
true,
),
];
for (held, rule, expect) in cases {
let held = caps(held);
assert_eq!(rule.satisfies(&held), expect, "{rule:?} against {held:?}");
assert_eq!(
rule.evaluate(&held).satisfied(),
expect,
"tree disagrees with bool: {rule:?}"
);
}
}
struct World {
stored: BTreeSet<Tech>,
level: u32,
}
impl Has<Tech> for World {
fn has(&self, key: &Tech) -> bool {
match key {
D => self.level >= 5,
stored => self.stored.contains(stored),
}
}
}
#[test]
fn a_computed_key_gets_one_answer_everywhere() {
let rule = Requirement::all([Requirement::has(A), Requirement::has(D)]);
let novice = World {
stored: BTreeSet::from([A]),
level: 1,
};
let veteran = World {
stored: BTreeSet::from([A]),
level: 5,
};
assert!(!rule.satisfies(&novice));
assert_eq!(rule.evaluate(&novice).missing(), vec![D]);
assert!(rule.satisfies(&veteran));
assert!(rule.evaluate(&veteran).satisfied());
let erased: &dyn Has<Tech> = &veteran;
assert!(rule.satisfies(erased));
}
#[test]
fn string_keys_need_no_rust_vocabulary() {
let held: BTreeSet<String> = ["warp".to_owned()].into_iter().collect();
assert!(Requirement::all([Requirement::has("warp".to_owned())]).satisfies(&held));
}
#[test]
fn missing_reports_an_unavoidable_shortfall_only() {
let held = caps(&[A]);
let cases: Vec<(Requirement<Tech>, Vec<Tech>)> = vec![
(Requirement::has(B), vec![B]),
(
Requirement::all([
Requirement::has(A),
Requirement::has(B),
Requirement::has(C),
]),
vec![B, C],
),
(
Requirement::all([Requirement::has(A), Requirement::all([Requirement::has(B)])]),
vec![B],
),
(Requirement::all([Requirement::has(A)]), vec![]),
(
Requirement::any([Requirement::has(B), Requirement::has(C)]),
vec![],
),
(Requirement::not(Requirement::has(A)), vec![]),
(
Requirement::at_least(2, [Requirement::has(B), Requirement::has(C)]),
vec![],
),
];
for (rule, expect) in cases {
assert_eq!(rule.evaluate(&held).missing(), expect, "{rule:?}");
}
}
#[test]
fn missing_names_each_key_once() {
let rule = Requirement::all([
Requirement::all([Requirement::has(A), Requirement::has(B)]),
Requirement::all([Requirement::has(A), Requirement::has(C)]),
]);
assert_eq!(rule.evaluate(&caps(&[])).missing(), vec![A, B, C]);
}
#[test]
fn required_and_optional_split_the_dependency_edges() {
let rule = Requirement::all([
Requirement::has(A),
Requirement::all([Requirement::has(B)]),
Requirement::any([
Requirement::has(A),
Requirement::has(C),
Requirement::has(D),
]),
Requirement::not(Requirement::has(D)),
]);
assert_eq!(
rule.required().iter().copied().collect::<Vec<_>>(),
vec![A, B]
);
assert_eq!(
rule.optional().iter().copied().collect::<Vec<_>>(),
vec![C, D]
);
}
#[test]
fn required_matches_missing_against_an_empty_set() {
let rule = Requirement::all([
Requirement::has(A),
Requirement::any([Requirement::has(B), Requirement::has(C)]),
]);
assert_eq!(
rule.required().iter().copied().collect::<Vec<_>>(),
rule.evaluate(&BTreeSet::new()).missing()
);
}
#[test]
fn a_choice_demands_no_one_key() {
let rule = Requirement::any([
Requirement::all([Requirement::has(A), Requirement::has(B)]),
Requirement::has(C),
]);
assert!(rule.required().is_empty());
assert_eq!(rule.optional().len(), 3);
}
#[test]
fn a_result_draws_itself_as_a_tree() {
let rule = Requirement::all([
Requirement::has(A),
Requirement::not(Requirement::has(B)),
Requirement::at_least(1, [Requirement::has(C)]),
]);
let drawn = rule.evaluate(&caps(&[A])).to_string();
assert_eq!(
drawn,
"\
✗ All of 3
✓ Has A
✓ Not
✗ Has B
✗ At least 1 of 1, 0 met
✗ Has C"
);
}
#[test]
fn warnings_report_authoring_traps() {
assert!(Requirement::<Tech>::any([]).warning().is_some());
assert!(
Requirement::at_least(3, [Requirement::has(A)])
.warning()
.unwrap()
.contains("never hold")
);
assert!(
Requirement::at_least(0, [Requirement::has(A)])
.warning()
.is_some()
);
assert!(Requirement::has(A).warning().is_none());
}
#[test]
fn warnings_name_the_offending_child() {
let rule = Requirement::all([Requirement::has(A), Requirement::any([])]);
assert!(rule.warning().unwrap().starts_with("Requirement 1:"));
let nested = Requirement::not(Requirement::<Tech>::any([]));
assert!(nested.warning().unwrap().contains("Inner requirement"));
}
}