use std::collections::HashMap;
use super::{Ruling, Scope, class_key};
use crate::control::classify::{self, Effect};
use crate::control::wire::Request;
use crate::opslog::{OpEntry, YOG_CONTROL};
const ANSWER: &str = "answer";
const FLOOR: &str = "floor";
const RAISE: &str = "raise";
const LOWER: &str = "lower";
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct Standing {
pub ruling: Ruling,
pub scope: Scope,
}
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct Answers {
once: HashMap<String, Ruling>,
standing: HashMap<(Scope, String, String), Ruling>,
floors: HashMap<String, bool>,
}
impl Answers {
pub fn fold(entries: &[OpEntry]) -> Answers {
let mut answers = Answers::default();
for entry in entries {
let words: Vec<&str> = entry.argv.iter().map(String::as_str).collect();
match words.as_slice() {
[YOG_CONTROL, ANSWER, key, word] => answers.answer(key, word, Scope::Call, entry),
[YOG_CONTROL, ANSWER, key, word, scope] => {
if let Some(scope) = Scope::of(scope) {
answers.answer(key, word, scope, entry);
}
}
[YOG_CONTROL, FLOOR, conv, state @ (RAISE | LOWER)] => {
answers.floors.insert((*conv).to_owned(), *state == RAISE);
}
_ => {}
}
}
answers
}
fn answer(&mut self, key: &str, word: &str, scope: Scope, entry: &OpEntry) {
let Some(ruling) = Ruling::of(word) else {
return;
};
match scope {
Scope::Call => {
self.once.insert(key.to_owned(), ruling);
}
Scope::Conversation => {
if let Some((conv, class)) = key.split_once(' ') {
self.standing
.insert((scope, conv.to_owned(), class.to_owned()), ruling);
}
}
Scope::Workspace => {
self.standing
.insert((scope, entry.cwd.clone(), key.to_owned()), ruling);
}
}
}
pub fn floored(&self, agent_id: &str) -> bool {
self.floors
.iter()
.any(|(conv, raised)| *raised && (agent_id == conv || descends(agent_id, conv)))
}
pub fn ruling(
&self,
request: &Request,
ws: &str,
effect: Effect,
policy: &super::super::policy::Policy,
) -> Standing {
if let Some(once) = self.once.get(&request.id) {
return Standing {
ruling: *once,
scope: Scope::Call,
};
}
let table = match classify::Leg::of(&request.name) {
classify::Leg::Engine => policy.ruling(effect),
classify::Leg::Routed => policy.ruling(effect).for_the_operator(),
};
if effect > Effect::Read
&& !classify::checkpoint(&request.name)
&& self.floored(&request.agent_id)
{
return Standing {
ruling: table.max(Ruling::Hold),
scope: Scope::Conversation,
};
}
let class = class_key(&request.name, effect);
self.nearest(&request.agent_id, &class)
.or_else(|| {
self.standing
.get(&(Scope::Workspace, ws.to_owned(), class))
.map(|ruling| Standing {
ruling: *ruling,
scope: Scope::Workspace,
})
})
.unwrap_or(Standing {
ruling: table,
scope: Scope::Workspace,
})
}
fn nearest(&self, agent_id: &str, class: &str) -> Option<Standing> {
self.standing
.iter()
.filter(|((scope, conv, key), _)| {
*scope == Scope::Conversation
&& key == class
&& (agent_id == conv || descends(agent_id, conv))
})
.max_by_key(|((_, conv, _), _)| conv.len())
.map(|(_, ruling)| Standing {
ruling: *ruling,
scope: Scope::Conversation,
})
}
}
fn descends(agent_id: &str, conv: &str) -> bool {
agent_id
.strip_prefix(conv)
.is_some_and(|rest| rest.starts_with('-'))
}
#[cfg(test)]
mod tests;