use super::cap::{Auth, Cap};
use super::error::BotError;
use std::future::Future;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[non_exhaustive]
pub enum RefreshReason {
Disconnected,
WatchOverflow,
StaleRemoteKey,
InvalidationFailed,
Superseded,
}
impl RefreshReason {
#[must_use]
pub const fn invalidates_baseline(self) -> bool {
!matches!(self, Self::Superseded)
}
#[must_use]
pub const fn as_str(self) -> &'static str {
match self {
Self::Disconnected => "disconnected",
Self::WatchOverflow => "watch-overflow",
Self::StaleRemoteKey => "stale-remote-key",
Self::InvalidationFailed => "invalidation-failed",
Self::Superseded => "superseded",
}
}
}
impl core::fmt::Display for RefreshReason {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
f.write_str(self.as_str())
}
}
pub trait Observe {
type Output;
fn required_caps(&self) -> &[Cap];
fn poll(&self, call: (Auth, ())) -> impl Future<Output = Result<Self::Output, BotError>>;
fn cache_state(&self) -> Option<RefreshReason> {
None
}
#[must_use]
fn revision(&self) -> Option<u64> {
None
}
#[deprecated(
since = "0.5.0",
note = "a detached fingerprint cannot be bound to an async poll result and is no longer used by EcsBot"
)]
fn fingerprint(&self) -> Option<u128> {
None
}
fn domain_id(&self) -> &str;
}
pub trait Evaluate<T> {
fn check(&self, value: &T) -> Result<bool, BotError>;
fn condition_id(&self) -> &str;
}
impl<T, F> Evaluate<T> for F
where
F: Fn(&T) -> bool,
{
fn check(&self, value: &T) -> Result<bool, BotError> {
Ok((self)(value))
}
fn condition_id(&self) -> &str {
"<closure>"
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[non_exhaustive]
pub enum EffectLifetime {
Local,
External,
}
pub trait Execute {
type Input;
type Output;
fn required_caps(&self) -> &[Cap];
fn effect_lifetime(&self) -> EffectLifetime {
EffectLifetime::External
}
fn execute_action(
&self,
call: (Auth, &Self::Input),
) -> impl Future<Output = Result<Self::Output, BotError>>;
fn domain_id(&self) -> &str;
}
pub trait Query {
type Input;
type Output;
fn required_caps(&self) -> &[Cap];
fn query(
&self,
call: (Auth, &Self::Input),
) -> impl Future<Output = Result<Self::Output, BotError>>;
fn domain_id(&self) -> &str;
}
#[cfg(test)]
mod tests {
use super::*;
const VERBS: [&str; 4] = ["Observe", "Evaluate", "Execute", "Query"];
fn declared_traits(source: &str) -> Vec<&str> {
let mut names = Vec::new();
for line in source.lines() {
let Some(rest) = line.trim().strip_prefix("pub trait ") else {
continue;
};
let Some(name) = rest
.split(|character: char| !(character.is_alphanumeric() || character == '_'))
.next()
.filter(|name: &&str| !name.is_empty())
else {
continue;
};
names.push(name);
}
names
}
fn reexported_from_crate_root(lib: &str) -> Vec<&str> {
let Some((_, after)) = lib.split_once("pub use verb::{") else {
return Vec::new();
};
let Some((names, _)) = after.split_once('}') else {
return Vec::new();
};
names
.split(',')
.map(str::trim)
.filter(|name| !name.is_empty())
.collect()
}
#[test]
fn the_module_declares_exactly_the_four_reviewed_verbs() {
let declared = declared_traits(include_str!("verb.rs"));
assert_eq!(
declared, VERBS,
"INV-BOT-FOUR-VERBS fixes this set; a fifth verb is a crate-level change"
);
}
#[test]
fn the_crate_root_reexports_exactly_those_four_verbs() {
let mut reexported = reexported_from_crate_root(include_str!("lib.rs"));
reexported.sort_unstable();
let mut expected = VERBS;
expected.sort_unstable();
assert_eq!(
reexported, expected,
"the crate root is the surface a fifth verb would arrive through"
);
}
#[test]
fn a_closure_is_still_an_evaluator() {
let above_one = |value: &u8| *value > 1;
let verdict = above_one.check(&2);
assert!(
matches!(verdict, Ok(true)),
"the blanket impl is what binds a bot spec to a condition: {verdict:?}"
);
assert_eq!(
above_one.condition_id(),
"<closure>",
"findings render this identifier, so it is part of the surface"
);
}
#[test]
fn only_supersession_leaves_the_baseline_sound() {
let cases: [(RefreshReason, bool, &str); 5] = [
(RefreshReason::Disconnected, true, "disconnected"),
(RefreshReason::WatchOverflow, true, "watch-overflow"),
(RefreshReason::StaleRemoteKey, true, "stale-remote-key"),
(
RefreshReason::InvalidationFailed,
true,
"invalidation-failed",
),
(RefreshReason::Superseded, false, "superseded"),
];
for (reason, invalidates, name) in cases {
assert_eq!(
reason.invalidates_baseline(),
invalidates,
"{name} invalidates the cached baseline: {invalidates}"
);
assert_eq!(reason.as_str(), name, "the report keys on this spelling");
assert_eq!(
reason.to_string(),
name,
"Display and the stable name are one spelling, not two"
);
}
}
}