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    }
82}
83
84/// 多 Fact 的多行输出
85pub fn facts_to_human(facts: &[Fact]) -> String {
86    facts
87        .iter()
88        .map(fact_to_human)
89        .collect::<Vec<_>>()
90        .join("\n")
91}
92
93/// diff 行前缀
94pub mod diff_prefix {
95    /// 两边都有但内容不同
96    pub const CHANGED: &str = "[~]";
97    /// 只在 A
98    pub const ONLY_A: &str = "[-]";
99    /// 只在 B
100    pub const ONLY_B: &str = "[+]";
101}
102
103/// 格式化 diff 行
104pub fn format_diff_line(prefix: &str, content: &str) -> String {
105    format!("{} {}", prefix, content)
106}
107
108#[cfg(test)]
109mod tests {
110    #![allow(clippy::unwrap_used)]
111    use super::*;
112    use evorule_reactor::{Fact, FactId, IoType};
113    use evorule_tcb::JsonValue;
114
115    #[test]
116    fn test_fact_to_human_command() {
117        let fact = Fact::Command {
118            id: FactId(1),
119            instruction: JsonValue::object_from_pairs(&[("type", JsonValue::string("noop"))]),
120        };
121        let s = fact_to_human(&fact);
122        assert!(s.contains("[F1] Command type=noop"));
123    }
124
125    #[test]
126    fn test_fact_to_human_error() {
127        let fact = Fact::Error {
128            id: FactId(5),
129            message: "max_steps exceeded".into(),
130        };
131        let s = fact_to_human(&fact);
132        assert!(s.contains("[F5] Error: max_steps exceeded"));
133    }
134
135    #[test]
136    fn test_fact_to_human_stable() {
137        let fact = Fact::Stable {
138            id: FactId(3),
139            version: 1,
140        };
141        let s = fact_to_human(&fact);
142        assert!(s.contains("[F3] Stable version=1"));
143    }
144
145    #[test]
146    fn test_fact_to_human_io_request() {
147        let fact = Fact::IoRequest {
148            id: FactId(4),
149            cause: FactId(2),
150            io_type: IoType::http_get(),
151            params: JsonValue::empty_object(),
152        };
153        let s = fact_to_human(&fact);
154        assert!(s.contains("IoRequest"));
155        assert!(s.contains("io_type=http_get"));
156    }
157
158    #[test]
159    fn test_facts_to_human_multiline() {
160        let facts = vec![
161            Fact::Command {
162                id: FactId(1),
163                instruction: JsonValue::empty_object(),
164            },
165            Fact::Stable {
166                id: FactId(2),
167                version: 1,
168            },
169        ];
170        let s = facts_to_human(&facts);
171        assert_eq!(s.lines().count(), 2);
172    }
173
174    #[test]
175    fn test_format_diff_line() {
176        let s = format_diff_line(diff_prefix::CHANGED, "some content");
177        assert_eq!(s, "[~] some content");
178    }
179}