use evorule_tcb::JsonValue;
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct FactId(pub u64);
impl core::fmt::Display for FactId {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
write!(f, "F{}", self.0)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
pub struct IoType(pub &'static str);
impl IoType {
pub const CALL_EXTERNAL: Self = IoType("call_external");
pub const QUERY_DB: Self = IoType("query_db");
pub const HTTP_GET: Self = IoType("http_get");
pub const SAVE_MEMORY: Self = IoType("save_memory");
pub const CALL_SERVICE: Self = IoType("call_service");
pub fn parse(s: &str) -> Option<Self> {
match s {
"call_external" => Some(IoType("call_external")),
"query_db" => Some(IoType("query_db")),
"http_get" => Some(IoType("http_get")),
"save_memory" => Some(IoType("save_memory")),
"call_service" => Some(IoType("call_service")),
_ => None,
}
}
pub fn as_str(&self) -> &str {
self.0
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum ControlFlowType {
Sequence,
Conditional,
WhileLoop,
Push,
}
impl ControlFlowType {
pub fn parse(s: &str) -> Option<Self> {
match s {
"sequence" => Some(ControlFlowType::Sequence),
"conditional" => Some(ControlFlowType::Conditional),
"while_loop" => Some(ControlFlowType::WhileLoop),
"push" => Some(ControlFlowType::Push),
_ => None,
}
}
pub fn as_str(&self) -> &'static str {
match self {
ControlFlowType::Sequence => "sequence",
ControlFlowType::Conditional => "conditional",
ControlFlowType::WhileLoop => "while_loop",
ControlFlowType::Push => "push",
}
}
}
impl core::fmt::Display for IoType {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
write!(f, "{}", self.as_str())
}
}
#[derive(Debug)]
pub struct FactIdGenerator {
next: u64,
}
impl FactIdGenerator {
pub const fn new() -> Self {
Self { next: 1 }
}
pub fn next_id(&mut self) -> FactId {
let id = FactId(self.next);
self.next += 1;
id
}
}
impl Default for FactIdGenerator {
fn default() -> Self {
Self::new()
}
}
#[derive(Debug, Clone, PartialEq)]
pub enum Fact {
Command {
id: FactId,
instruction: JsonValue,
},
PayloadUpdate {
id: FactId,
path: String,
value: JsonValue,
},
StateTransition {
id: FactId,
cause: FactId,
new_payload: JsonValue,
new_queue: Vec<JsonValue>,
},
IoRequest {
id: FactId,
cause: FactId,
io_type: IoType,
params: JsonValue,
},
IoResponse {
id: FactId,
request_id: FactId,
result: JsonValue,
error: Option<String>,
},
Stable {
id: FactId,
final_snapshot: JsonValue,
},
Error {
id: FactId,
message: String,
},
}
impl Fact {
pub fn type_name(&self) -> &'static str {
match self {
Fact::Command { .. } => "Command",
Fact::PayloadUpdate { .. } => "PayloadUpdate",
Fact::StateTransition { .. } => "StateTransition",
Fact::IoRequest { .. } => "IoRequest",
Fact::IoResponse { .. } => "IoResponse",
Fact::Stable { .. } => "Stable",
Fact::Error { .. } => "Error",
}
}
pub fn id(&self) -> FactId {
match self {
Fact::Command { id, .. }
| Fact::PayloadUpdate { id, .. }
| Fact::StateTransition { id, .. }
| Fact::IoRequest { id, .. }
| Fact::IoResponse { id, .. }
| Fact::Stable { id, .. }
| Fact::Error { id, .. } => *id,
}
}
pub fn is_terminal(&self) -> bool {
matches!(self, Fact::Stable { .. } | Fact::Error { .. })
}
pub fn to_json(&self) -> JsonValue {
use evorule_tcb::JsonValue as J;
match self {
Fact::Command { id, instruction } => J::object_from_pairs(&[
("type", J::string("Command")),
("id", J::integer(id.0 as i64)),
("instruction", instruction.clone()),
]),
Fact::PayloadUpdate { id, path, value } => J::object_from_pairs(&[
("type", J::string("PayloadUpdate")),
("id", J::integer(id.0 as i64)),
("path", J::string(path.clone())),
("value", value.clone()),
]),
Fact::StateTransition {
id,
cause,
new_payload,
new_queue,
} => J::object_from_pairs(&[
("type", J::string("StateTransition")),
("id", J::integer(id.0 as i64)),
("cause", J::integer(cause.0 as i64)),
("new_payload", new_payload.clone()),
("new_queue", J::array(new_queue.clone())),
]),
Fact::IoRequest {
id,
cause,
io_type,
params,
} => J::object_from_pairs(&[
("type", J::string("IoRequest")),
("id", J::integer(id.0 as i64)),
("cause", J::integer(cause.0 as i64)),
("io_type", J::string(io_type.as_str())),
("params", params.clone()),
]),
Fact::IoResponse {
id,
request_id,
result,
error,
} => {
let pairs: Vec<(&str, J)> = vec![
("type", J::string("IoResponse")),
("id", J::integer(id.0 as i64)),
("request_id", J::integer(request_id.0 as i64)),
("result", result.clone()),
(
"error",
match error {
Some(msg) => J::string(msg.clone()),
None => J::null(),
},
),
];
J::object_from_pairs(&pairs)
}
Fact::Stable { id, final_snapshot } => J::object_from_pairs(&[
("type", J::string("Stable")),
("id", J::integer(id.0 as i64)),
("final_snapshot", final_snapshot.clone()),
]),
Fact::Error { id, message } => J::object_from_pairs(&[
("type", J::string("Error")),
("id", J::integer(id.0 as i64)),
("message", J::string(message.clone())),
]),
}
}
}
#[cfg(test)]
mod tests {
#![allow(clippy::unwrap_used)]
#![allow(clippy::panic, clippy::expect_used)]
use super::*;
#[test]
fn test_fact_id_generator() {
let mut gen = FactIdGenerator::new();
assert_eq!(gen.next_id(), FactId(1));
assert_eq!(gen.next_id(), FactId(2));
assert_eq!(gen.next_id(), FactId(3));
}
#[test]
fn test_fact_type_name_and_id() {
let fact = Fact::Command {
id: FactId(1),
instruction: JsonValue::empty_object(),
};
assert_eq!(fact.type_name(), "Command");
assert_eq!(fact.id(), FactId(1));
let fact = Fact::Stable {
id: FactId(2),
final_snapshot: JsonValue::empty_object(),
};
assert_eq!(fact.type_name(), "Stable");
assert_eq!(fact.id(), FactId(2));
assert!(fact.is_terminal());
}
#[test]
fn test_fact_cause_field() {
let transition = Fact::StateTransition {
id: FactId(10),
cause: FactId(5),
new_payload: JsonValue::empty_object(),
new_queue: vec![],
};
assert_eq!(transition.id(), FactId(10));
let io_req = Fact::IoRequest {
id: FactId(20),
cause: FactId(10),
io_type: IoType::CALL_EXTERNAL,
params: JsonValue::empty_object(),
};
assert_eq!(io_req.id(), FactId(20));
}
#[test]
fn test_io_response_with_error() {
let ok_resp = Fact::IoResponse {
id: FactId(1),
request_id: FactId(2),
result: JsonValue::string("ok"),
error: None,
};
assert_eq!(ok_resp.type_name(), "IoResponse");
assert!(!ok_resp.is_terminal());
let err_resp = Fact::IoResponse {
id: FactId(2),
request_id: FactId(2),
result: JsonValue::Null,
error: Some("timeout".to_string()),
};
assert_eq!(err_resp.id(), FactId(2));
}
#[test]
fn test_io_type_roundtrip() {
for expected in [
IoType::CALL_EXTERNAL,
IoType::QUERY_DB,
IoType::HTTP_GET,
IoType::SAVE_MEMORY,
IoType::CALL_SERVICE,
] {
let s = expected.as_str();
let parsed = IoType::parse(s).expect("roundtrip should succeed");
assert_eq!(parsed, expected, "roundtrip failed for {}", s);
}
}
#[test]
fn test_io_type_parse_unknown() {
assert!(IoType::parse("unknown").is_none());
assert!(IoType::parse("").is_none());
assert!(IoType::parse("CALL_LLM").is_none());
}
#[test]
fn test_io_type_display() {
assert_eq!(format!("{}", IoType::CALL_EXTERNAL), "call_external");
assert_eq!(format!("{}", IoType::QUERY_DB), "query_db");
assert_eq!(format!("{}", IoType::HTTP_GET), "http_get");
assert_eq!(format!("{}", IoType::SAVE_MEMORY), "save_memory");
assert_eq!(format!("{}", IoType::CALL_SERVICE), "call_service");
}
#[test]
fn test_fact_id_display() {
assert_eq!(format!("{}", FactId(0)), "F0");
assert_eq!(format!("{}", FactId(1)), "F1");
assert_eq!(format!("{}", FactId(42)), "F42");
}
#[test]
fn test_fact_is_terminal_all_variants() {
assert!(Fact::Stable {
id: FactId(1),
final_snapshot: JsonValue::empty_object(),
}
.is_terminal());
assert!(Fact::Error {
id: FactId(2),
message: "err".to_string(),
}
.is_terminal());
assert!(!Fact::Command {
id: FactId(3),
instruction: JsonValue::empty_object(),
}
.is_terminal());
assert!(!Fact::PayloadUpdate {
id: FactId(4),
path: "x".to_string(),
value: JsonValue::Null,
}
.is_terminal());
assert!(!Fact::StateTransition {
id: FactId(5),
cause: FactId(0),
new_payload: JsonValue::empty_object(),
new_queue: vec![],
}
.is_terminal());
assert!(!Fact::IoRequest {
id: FactId(6),
cause: FactId(0),
io_type: IoType::CALL_EXTERNAL,
params: JsonValue::empty_object(),
}
.is_terminal());
assert!(!Fact::IoResponse {
id: FactId(7),
request_id: FactId(6),
result: JsonValue::Null,
error: None,
}
.is_terminal());
}
#[test]
fn test_fact_type_name_all_variants() {
assert_eq!(
Fact::Command {
id: FactId(1),
instruction: JsonValue::empty_object(),
}
.type_name(),
"Command"
);
assert_eq!(
Fact::PayloadUpdate {
id: FactId(1),
path: "x".to_string(),
value: JsonValue::Null,
}
.type_name(),
"PayloadUpdate"
);
assert_eq!(
Fact::StateTransition {
id: FactId(1),
cause: FactId(0),
new_payload: JsonValue::empty_object(),
new_queue: vec![],
}
.type_name(),
"StateTransition"
);
assert_eq!(
Fact::IoRequest {
id: FactId(1),
cause: FactId(0),
io_type: IoType::CALL_EXTERNAL,
params: JsonValue::empty_object(),
}
.type_name(),
"IoRequest"
);
assert_eq!(
Fact::IoResponse {
id: FactId(1),
request_id: FactId(0),
result: JsonValue::Null,
error: None,
}
.type_name(),
"IoResponse"
);
assert_eq!(
Fact::Stable {
id: FactId(1),
final_snapshot: JsonValue::empty_object(),
}
.type_name(),
"Stable"
);
assert_eq!(
Fact::Error {
id: FactId(1),
message: "e".to_string(),
}
.type_name(),
"Error"
);
}
#[test]
fn test_fact_id_generator_default() {
let mut gen = FactIdGenerator::default();
assert_eq!(gen.next_id(), FactId(1));
}
}