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, PartialEq, Eq, Hash, PartialOrd, Ord)]
pub struct IoType(pub std::sync::Arc<str>);
impl IoType {
pub fn call_external() -> Self {
Self(std::sync::Arc::from("call_external"))
}
pub fn query_db() -> Self {
Self(std::sync::Arc::from("query_db"))
}
pub fn http_get() -> Self {
Self(std::sync::Arc::from("http_get"))
}
pub fn save_memory() -> Self {
Self(std::sync::Arc::from("save_memory"))
}
pub fn call_service() -> Self {
Self(std::sync::Arc::from("call_service"))
}
pub fn new(name: &str) -> Self {
Self(std::sync::Arc::from(name))
}
#[deprecated(note = "v0.2.0 起用 IoType::new;parse 不再校验,保留仅为向后兼容")]
pub fn parse(s: &str) -> Option<Self> {
Some(Self::new(s))
}
pub fn as_str(&self) -> &str {
self.0.as_ref()
}
}
#[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 resume(from: u64) -> Self {
Self { next: from }
}
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_id_generator_resume() {
let mut gen = FactIdGenerator::resume(100);
assert_eq!(gen.next_id(), FactId(100));
assert_eq!(gen.next_id(), FactId(101));
assert_eq!(gen.next_id(), FactId(102));
}
#[test]
fn test_fact_id_generator_resume_from_1_equals_new() {
let mut a = FactIdGenerator::new();
let mut b = FactIdGenerator::resume(1);
for _ in 0..5 {
assert_eq!(a.next_id(), b.next_id());
}
}
#[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 constructed = IoType::new(s);
assert_eq!(constructed, expected, "roundtrip failed for {}", s);
}
}
#[test]
fn test_io_type_parse_accepts_any() {
#[allow(deprecated)]
{
assert!(IoType::parse("unknown").is_some());
assert!(IoType::parse("retrieve").is_some());
assert!(IoType::parse("call_service").is_some());
assert!(IoType::parse("").is_some());
}
}
#[test]
fn test_io_type_new_equals_factory() {
assert_eq!(IoType::new("call_service"), IoType::call_service());
assert_eq!(IoType::new("call_external"), IoType::call_external());
assert_eq!(IoType::new("retrieve"), IoType::new("retrieve"));
assert_ne!(IoType::new("retrieve"), IoType::new("file"));
}
#[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));
}
}