Skip to main content

evorule_cli/
output.rs

1// SPDX-License-Identifier: AGPL-3.0-or-later
2// Copyright (C) 2026 EvoRule Project
3// This file is part of EvoRule, licensed under GNU Affero General Public License v3 or later.
4//! 输出格式化(human-readable + diff)
5//!
6//! # 设计
7//! - `fact_to_human`:单个 Fact 的单行摘要(适合 replay/diff)
8//! - `facts_to_human`:多 Fact 的多行输出
9//! - `format_diff_line`:diff 行前缀(`[~]`/`[-]`/`[+]`)
10
11use evorule_reactor::Fact;
12
13/// 单个 Fact 的单行摘要
14///
15/// 格式:`[F{id}] {type} {detail}`
16/// - Command: `[F1] Command type=noop`
17/// - StateTransition: `[F2] StateTransition cause=F1`
18/// - IoRequest: `[F3] IoRequest io_type=call_external`
19/// - Stable: `[F4] Stable`
20/// - Error: `[F5] Error: max_steps exceeded`
21///
22/// # 示例
23/// ```
24/// use evorule_cli::output::fact_to_human;
25/// use evorule_reactor::{Fact, FactId};
26/// use evorule_tcb::JsonValue;
27///
28/// let command = Fact::Command {
29///     id: FactId(1),
30///     instruction: JsonValue::object_from_pairs(&[
31///         ("type", JsonValue::string("noop")),
32///     ]),
33/// };
34/// assert_eq!(fact_to_human(&command), "[F1] Command type=noop");
35///
36/// let stable = Fact::Stable {
37///     id: FactId(4),
38///     version: 1,
39/// };
40/// assert_eq!(fact_to_human(&stable), "[F4] Stable version=1");
41/// ```
42pub 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
101/// 多 Fact 的多行输出
102pub fn facts_to_human(facts: &[Fact]) -> String {
103    facts
104        .iter()
105        .map(fact_to_human)
106        .collect::<Vec<_>>()
107        .join("\n")
108}
109
110/// diff 行前缀
111pub mod diff_prefix {
112    /// 两边都有但内容不同
113    pub const CHANGED: &str = "[~]";
114    /// 只在 A
115    pub const ONLY_A: &str = "[-]";
116    /// 只在 B
117    pub const ONLY_B: &str = "[+]";
118}
119
120/// 格式化 diff 行
121pub 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}