1use evorule_reactor::Fact;
12
13pub fn fact_to_human(fact: &Fact) -> String {
43 match fact {
44 Fact::Command { id, instruction } => {
45 let instr_type = instruction
46 .get("type")
47 .and_then(|v| v.as_str())
48 .unwrap_or("?");
49 format!("[F{}] Command type={}", id.0, instr_type)
50 }
51 Fact::PayloadUpdate { id, path, .. } => {
52 format!("[F{}] PayloadUpdate path={}", id.0, path)
53 }
54 Fact::StateTransition { id, cause, .. } => {
55 format!("[F{}] StateTransition cause=F{}", id.0, cause.0)
56 }
57 Fact::IoRequest {
58 id, cause, io_type, ..
59 } => {
60 format!(
61 "[F{}] IoRequest cause=F{} io_type={}",
62 id.0,
63 cause.0,
64 io_type.as_str()
65 )
66 }
67 Fact::IoResponse {
68 id,
69 request_id,
70 error,
71 ..
72 } => match error {
73 Some(msg) => format!(
74 "[F{}] IoResponse request_id=F{} error={}",
75 id.0, request_id.0, msg
76 ),
77 None => format!("[F{}] IoResponse request_id=F{} ok", id.0, request_id.0),
78 },
79 Fact::Stable { id, version } => format!("[F{}] Stable version={}", id.0, version),
80 Fact::Error { id, message } => format!("[F{}] Error: {}", id.0, message),
81 Fact::TransitionTrace { id, cause, rule_hits } => {
82 let hit_count = rule_hits.iter().filter(|h| h.hit).count();
83 format!(
84 "[F{}] TransitionTrace cause=F{} hits={}/{}",
85 id.0,
86 cause.0,
87 hit_count,
88 rule_hits.len()
89 )
90 }
91 Fact::Violation { id, cause, rule_index, reason, .. } => format!(
92 "[F{}] Violation cause=F{} rule={} {}",
93 id.0,
94 cause.0,
95 rule_index,
96 reason
97 ),
98 }
99}
100
101pub fn facts_to_human(facts: &[Fact]) -> String {
103 facts
104 .iter()
105 .map(fact_to_human)
106 .collect::<Vec<_>>()
107 .join("\n")
108}
109
110pub mod diff_prefix {
112 pub const CHANGED: &str = "[~]";
114 pub const ONLY_A: &str = "[-]";
116 pub const ONLY_B: &str = "[+]";
118}
119
120pub fn format_diff_line(prefix: &str, content: &str) -> String {
122 format!("{} {}", prefix, content)
123}
124
125#[cfg(test)]
126mod tests {
127 #![allow(clippy::unwrap_used)]
128 use super::*;
129 use evorule_reactor::{Fact, FactId, IoType};
130 use evorule_tcb::JsonValue;
131
132 #[test]
133 fn test_fact_to_human_command() {
134 let fact = Fact::Command {
135 id: FactId(1),
136 instruction: JsonValue::object_from_pairs(&[("type", JsonValue::string("noop"))]),
137 };
138 let s = fact_to_human(&fact);
139 assert!(s.contains("[F1] Command type=noop"));
140 }
141
142 #[test]
143 fn test_fact_to_human_error() {
144 let fact = Fact::Error {
145 id: FactId(5),
146 message: "max_steps exceeded".into(),
147 };
148 let s = fact_to_human(&fact);
149 assert!(s.contains("[F5] Error: max_steps exceeded"));
150 }
151
152 #[test]
153 fn test_fact_to_human_stable() {
154 let fact = Fact::Stable {
155 id: FactId(3),
156 version: 1,
157 };
158 let s = fact_to_human(&fact);
159 assert!(s.contains("[F3] Stable version=1"));
160 }
161
162 #[test]
163 fn test_fact_to_human_io_request() {
164 let fact = Fact::IoRequest {
165 id: FactId(4),
166 cause: FactId(2),
167 io_type: IoType::http_get(),
168 params: JsonValue::empty_object(),
169 };
170 let s = fact_to_human(&fact);
171 assert!(s.contains("IoRequest"));
172 assert!(s.contains("io_type=http_get"));
173 }
174
175 #[test]
176 fn test_facts_to_human_multiline() {
177 let facts = vec![
178 Fact::Command {
179 id: FactId(1),
180 instruction: JsonValue::empty_object(),
181 },
182 Fact::Stable {
183 id: FactId(2),
184 version: 1,
185 },
186 ];
187 let s = facts_to_human(&facts);
188 assert_eq!(s.lines().count(), 2);
189 }
190
191 #[test]
192 fn test_format_diff_line() {
193 let s = format_diff_line(diff_prefix::CHANGED, "some content");
194 assert_eq!(s, "[~] some content");
195 }
196}