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 {
82 id,
83 cause,
84 rule_hits,
85 } => {
86 let hit_count = rule_hits.iter().filter(|h| h.hit).count();
87 format!(
88 "[F{}] TransitionTrace cause=F{} hits={}/{}",
89 id.0,
90 cause.0,
91 hit_count,
92 rule_hits.len()
93 )
94 }
95 Fact::Violation {
96 id,
97 cause,
98 rule_index,
99 reason,
100 ..
101 } => format!(
102 "[F{}] Violation cause=F{} rule={} {}",
103 id.0, cause.0, rule_index, reason
104 ),
105 }
106}
107
108pub fn facts_to_human(facts: &[Fact]) -> String {
110 facts
111 .iter()
112 .map(fact_to_human)
113 .collect::<Vec<_>>()
114 .join("\n")
115}
116
117pub mod diff_prefix {
119 pub const CHANGED: &str = "[~]";
121 pub const ONLY_A: &str = "[-]";
123 pub const ONLY_B: &str = "[+]";
125}
126
127pub fn format_diff_line(prefix: &str, content: &str) -> String {
129 format!("{} {}", prefix, content)
130}
131
132#[cfg(test)]
133mod tests {
134 #![allow(clippy::unwrap_used)]
135 use super::*;
136 use evorule_reactor::{Fact, FactId, IoType};
137 use evorule_tcb::JsonValue;
138
139 #[test]
140 fn test_fact_to_human_command() {
141 let fact = Fact::Command {
142 id: FactId(1),
143 instruction: JsonValue::object_from_pairs(&[("type", JsonValue::string("noop"))]),
144 };
145 let s = fact_to_human(&fact);
146 assert!(s.contains("[F1] Command type=noop"));
147 }
148
149 #[test]
150 fn test_fact_to_human_error() {
151 let fact = Fact::Error {
152 id: FactId(5),
153 message: "max_steps exceeded".into(),
154 };
155 let s = fact_to_human(&fact);
156 assert!(s.contains("[F5] Error: max_steps exceeded"));
157 }
158
159 #[test]
160 fn test_fact_to_human_stable() {
161 let fact = Fact::Stable {
162 id: FactId(3),
163 version: 1,
164 };
165 let s = fact_to_human(&fact);
166 assert!(s.contains("[F3] Stable version=1"));
167 }
168
169 #[test]
170 fn test_fact_to_human_io_request() {
171 let fact = Fact::IoRequest {
172 id: FactId(4),
173 cause: FactId(2),
174 io_type: IoType::http_get(),
175 params: JsonValue::empty_object(),
176 };
177 let s = fact_to_human(&fact);
178 assert!(s.contains("IoRequest"));
179 assert!(s.contains("io_type=http_get"));
180 }
181
182 #[test]
183 fn test_facts_to_human_multiline() {
184 let facts = vec![
185 Fact::Command {
186 id: FactId(1),
187 instruction: JsonValue::empty_object(),
188 },
189 Fact::Stable {
190 id: FactId(2),
191 version: 1,
192 },
193 ];
194 let s = facts_to_human(&facts);
195 assert_eq!(s.lines().count(), 2);
196 }
197
198 #[test]
199 fn test_format_diff_line() {
200 let s = format_diff_line(diff_prefix::CHANGED, "some content");
201 assert_eq!(s, "[~] some content");
202 }
203}