use std::cell::RefCell;
use std::collections::HashMap;
use brink_format::Value;
use crate::story::{ExternalFnHandler, ExternalResult};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum PolicyKind {
Query,
Effect,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum EvalContext {
Watch,
Eval,
}
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct ExternalsReport {
pub live: Vec<String>,
pub fallback: Vec<String>,
}
pub struct KindTieredHandler<'h> {
inner: &'h dyn ExternalFnHandler,
kinds: HashMap<String, PolicyKind>,
context: EvalContext,
live_effects: bool,
report: RefCell<ExternalsReport>,
}
impl<'h> KindTieredHandler<'h> {
#[must_use]
pub fn new(
inner: &'h dyn ExternalFnHandler,
kinds: HashMap<String, PolicyKind>,
context: EvalContext,
live_effects: bool,
) -> Self {
Self {
inner,
kinds,
context,
live_effects,
report: RefCell::new(ExternalsReport::default()),
}
}
#[must_use]
pub fn report(&self) -> ExternalsReport {
self.report.borrow().clone()
}
fn armed(&self, kind: PolicyKind) -> bool {
match kind {
PolicyKind::Query => true,
PolicyKind::Effect => self.context == EvalContext::Eval && self.live_effects,
}
}
}
impl ExternalFnHandler for KindTieredHandler<'_> {
fn call(&self, name: &str, args: &[Value]) -> ExternalResult {
let kind = self.kinds.get(name).copied().unwrap_or(PolicyKind::Effect);
if self.armed(kind) {
self.report.borrow_mut().live.push(name.to_owned());
self.inner.call(name, args)
} else {
self.report.borrow_mut().fallback.push(name.to_owned());
ExternalResult::Fallback
}
}
}
#[cfg(test)]
mod tests {
use super::*;
struct Stub;
impl ExternalFnHandler for Stub {
fn call(&self, name: &str, _args: &[Value]) -> ExternalResult {
match name {
"async" => ExternalResult::Pending,
"get" => ExternalResult::Resolved(Value::Int(5)),
"act" => ExternalResult::Resolved(Value::Bool(true)),
_ => ExternalResult::Fallback,
}
}
}
fn kinds(pairs: &[(&str, PolicyKind)]) -> HashMap<String, PolicyKind> {
pairs.iter().map(|(n, k)| ((*n).to_owned(), *k)).collect()
}
#[test]
fn query_delegates_live_including_pending() {
let inner = Stub;
let h = KindTieredHandler::new(
&inner,
kinds(&[("get", PolicyKind::Query), ("async", PolicyKind::Query)]),
EvalContext::Watch,
false,
);
assert!(matches!(
h.call("get", &[]),
ExternalResult::Resolved(Value::Int(5))
));
assert!(matches!(h.call("async", &[]), ExternalResult::Pending));
assert_eq!(h.report().live, vec!["get".to_owned(), "async".to_owned()]);
assert!(h.report().fallback.is_empty());
}
#[test]
fn effect_under_watch_falls_back_inner_not_called() {
let inner = Stub;
let h = KindTieredHandler::new(
&inner,
kinds(&[("act", PolicyKind::Effect)]),
EvalContext::Watch,
true, );
assert!(matches!(h.call("act", &[]), ExternalResult::Fallback));
assert_eq!(h.report().fallback, vec!["act".to_owned()]);
assert!(h.report().live.is_empty());
}
#[test]
fn effect_under_eval_disarmed_falls_back() {
let inner = Stub;
let h = KindTieredHandler::new(
&inner,
kinds(&[("act", PolicyKind::Effect)]),
EvalContext::Eval,
false,
);
assert!(matches!(h.call("act", &[]), ExternalResult::Fallback));
assert_eq!(h.report().fallback, vec!["act".to_owned()]);
}
#[test]
fn effect_under_eval_armed_runs_live() {
let inner = Stub;
let h = KindTieredHandler::new(
&inner,
kinds(&[("act", PolicyKind::Effect)]),
EvalContext::Eval,
true,
);
assert!(matches!(
h.call("act", &[]),
ExternalResult::Resolved(Value::Bool(true))
));
assert_eq!(h.report().live, vec!["act".to_owned()]);
}
#[test]
fn unclassified_name_treated_as_effect() {
let inner = Stub;
let h = KindTieredHandler::new(&inner, kinds(&[]), EvalContext::Watch, false);
assert!(matches!(h.call("act", &[]), ExternalResult::Fallback));
assert_eq!(h.report().fallback, vec!["act".to_owned()]);
let h2 = KindTieredHandler::new(&inner, kinds(&[]), EvalContext::Eval, true);
assert!(matches!(
h2.call("act", &[]),
ExternalResult::Resolved(Value::Bool(true))
));
assert_eq!(h2.report().live, vec!["act".to_owned()]);
}
#[test]
fn report_reflects_mixed_live_and_fallback_in_call_order() {
let inner = Stub;
let h = KindTieredHandler::new(
&inner,
kinds(&[("get", PolicyKind::Query), ("act", PolicyKind::Effect)]),
EvalContext::Watch,
false,
);
let _ = h.call("get", &[]);
let _ = h.call("act", &[]);
let _ = h.call("get", &[]);
let report = h.report();
assert_eq!(report.live, vec!["get".to_owned(), "get".to_owned()]);
assert_eq!(report.fallback, vec!["act".to_owned()]);
}
}