use std::collections::BTreeMap;
use sim_kernel::{Error, Result, Symbol};
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum ReplyRule {
Opens,
Any,
AnyNonReceipt,
Only(Vec<Symbol>),
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct BridgeMoveSpec {
pub intent: Symbol,
pub replies_to: ReplyRule,
pub requires_parts: Vec<Symbol>,
pub requires_any_parts: Vec<Vec<Symbol>>,
pub terminal: bool,
}
#[derive(Clone, Debug, Default, PartialEq, Eq)]
pub struct BridgeMoveBook {
moves: BTreeMap<Symbol, BridgeMoveSpec>,
}
impl BridgeMoveBook {
pub fn new() -> Self {
Self::default()
}
pub fn register(&mut self, spec: BridgeMoveSpec) {
self.moves.insert(spec.intent.clone(), spec);
}
pub fn spec(&self, intent: &Symbol) -> Option<&BridgeMoveSpec> {
self.moves.get(intent)
}
pub fn specs(&self) -> impl Iterator<Item = &BridgeMoveSpec> {
self.moves.values()
}
pub fn legal_reply_intents(&self, parents: &[Symbol]) -> Vec<Symbol> {
self.moves
.values()
.filter(|spec| check_reply_rule(&spec.intent, &spec.replies_to, parents).is_ok())
.map(|spec| spec.intent.clone())
.collect()
}
pub fn check_move(&self, intent: &Symbol, parents: &[Symbol], parts: &[Symbol]) -> Result<()> {
let spec = self
.moves
.get(intent)
.ok_or_else(|| Error::Eval(format!("unknown BRIDGE move {intent}")))?;
check_reply_rule(intent, &spec.replies_to, parents)?;
for required in &spec.requires_parts {
if !parts.contains(required) {
return Err(Error::Eval(format!("{intent} requires a {required} part")));
}
}
for alternatives in &spec.requires_any_parts {
if alternatives.iter().any(|required| parts.contains(required)) {
continue;
}
let names = alternatives
.iter()
.map(Symbol::as_qualified_str)
.collect::<Vec<_>>()
.join(" or ");
return Err(Error::Eval(format!("{intent} requires {names}")));
}
Ok(())
}
}
pub fn standard_move_book() -> BridgeMoveBook {
let mut book = BridgeMoveBook::new();
for spec in [
move_spec_with_any(
"request",
ReplyRule::Opens,
&["Return"],
&[&["Frame", "Call", "Weave"]],
false,
),
move_spec(
"offer",
ReplyRule::Only(vec![intent("request")]),
&["Return"],
false,
),
move_spec(
"reply",
ReplyRule::Only(vec![intent("request")]),
&["Return"],
false,
),
move_spec(
"review",
ReplyRule::Only(vec![intent("offer"), intent("reply"), intent("patch")]),
&["Review"],
false,
),
move_spec(
"vote",
ReplyRule::Only(vec![intent("offer"), intent("reply")]),
&["Vote"],
false,
),
move_spec(
"patch",
ReplyRule::Only(vec![intent("receipt"), intent("review"), intent("reply")]),
&["Patch"],
false,
),
move_spec("fetch", ReplyRule::AnyNonReceipt, &["Fetch"], false),
move_spec("receipt", ReplyRule::AnyNonReceipt, &["Receipt"], true),
move_spec(
"attest",
ReplyRule::Only(vec![intent("reply"), intent("receipt")]),
&["Attest"],
true,
),
move_spec("error", ReplyRule::Any, &["Receipt"], true),
] {
book.register(spec);
}
book
}
fn check_reply_rule(current_intent: &Symbol, rule: &ReplyRule, parents: &[Symbol]) -> Result<()> {
match rule {
ReplyRule::Opens => {
if parents.is_empty() {
Ok(())
} else {
Err(Error::Eval(format!(
"{current_intent} opens a thread; it cannot reply"
)))
}
}
ReplyRule::Any => require_parent(current_intent, parents),
ReplyRule::AnyNonReceipt => {
require_parent(current_intent, parents)?;
for parent in parents {
if parent == &intent("receipt") {
return Err(Error::Eval(format!(
"{current_intent} may not answer {parent}"
)));
}
}
Ok(())
}
ReplyRule::Only(allowed) => {
require_parent(current_intent, parents)?;
for parent in parents {
if !allowed.contains(parent) {
return Err(Error::Eval(format!(
"{current_intent} may not answer {parent}"
)));
}
}
Ok(())
}
}
}
fn require_parent(intent: &Symbol, parents: &[Symbol]) -> Result<()> {
if parents.is_empty() {
Err(Error::Eval(format!("{intent} requires a parent move")))
} else {
Ok(())
}
}
fn move_spec(
name: &str,
replies_to: ReplyRule,
required_parts: &[&str],
terminal: bool,
) -> BridgeMoveSpec {
move_spec_with_any(name, replies_to, required_parts, &[], terminal)
}
fn move_spec_with_any(
name: &str,
replies_to: ReplyRule,
required_parts: &[&str],
required_any_parts: &[&[&str]],
terminal: bool,
) -> BridgeMoveSpec {
BridgeMoveSpec {
intent: intent(name),
replies_to,
requires_parts: required_parts
.iter()
.map(|name| Symbol::qualified("bridge", *name))
.collect(),
requires_any_parts: required_any_parts
.iter()
.map(|group| {
group
.iter()
.map(|name| Symbol::qualified("bridge", *name))
.collect()
})
.collect(),
terminal,
}
}
fn intent(name: &str) -> Symbol {
Symbol::new(name)
}