diff --git a/crates/core/src/types.rs b/crates/core/src/types.rs index f60c035..68c9c91 100644 --- a/crates/core/src/types.rs +++ b/crates/core/src/types.rs @@ -1,112 +1,252 @@ +// ─── types.rs ───────────────────────────────────────────────────────────────── +// 这个文件定义了 log-viewer-core 库中使用的核心数据类型(数据模型)。 +// 包括:日志级别(LogLevel)、日志条目(LogEntry)、搜索结果(SearchResult)、 +// 搜索查询(SearchQuery)、搜索过滤器(SearchFilter)、书签(Bookmark)、 +// 文件会话(FileSession)等。 +// +// 这些类型是整个应用的"通用语言",各个模块(io、parser、tui 等)都依赖它们 +// 来传递和处理数据。 +// ────────────────────────────────────────────────────────────────────────────── + +// HashMap — 哈希表(字典)类型,用于存储键值对。 +// 这里用于存储日志条目中除 timestamp/level 之外的其他 JSON 字段。 use std::collections::HashMap; +// fmt — 格式化模块,提供 Display trait(自定义 .to_string() 行为)。 use std::fmt; +// FromStr — 字符串解析 trait,允许从字符串解析为自定义类型(如 "INFO" → LogLevel::Info)。 use std::str::FromStr; +// serde 是 Rust 中最流行的序列化/反序列化框架。 +// - Serialize: 将 Rust 数据结构转换为其他格式(如 JSON、TOML)。 +// - Deserialize: 将其他格式(如 JSON、TOML)转换回 Rust 数据结构。 +// 这样 LogLevel、LogEntry 等类型就可以被序列化为 JSON 保存,或从 JSON 反序列化恢复。 use serde::{Deserialize, Serialize}; +// serde_json::Value — 可以表示任意 JSON 值的枚举类型。 use serde_json::Value; +// ─── LogLevel 枚举 ────────────────────────────────────────────────────────── /// 日志级别 +/// +/// 表示一条日志的严重程度。不同日志系统使用不同的级别名称, +/// 这里统一定义为 5 个标准级别 + 1 个"未知"级别。 +/// +/// # 派生属性说明 +/// - `Debug`: 允许用 `{:?}` 格式化打印,用于调试。 +/// - `Clone`: 允许克隆(深拷贝)该值。 +/// - `PartialEq`: 允许用 `==` 和 `!=` 比较两个值是否相等。 +/// - `Eq`: 表示"完全相等"关系(PartialEq 的增强版,要求满足等价关系的数学性质)。 +/// - `Hash`: 允许将该值用作 HashMap 的键(需要 Eq + Hash 配合)。 +/// - `Serialize`: 允许序列化为 JSON 等格式(如 `"Info"` → JSON 字符串 `"Info"`)。 +/// - `Deserialize`: 允许从 JSON 等格式反序列化(如 JSON 字符串 `"Info"` → `LogLevel::Info`)。 #[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)] pub enum LogLevel { - Error, - Warn, - Info, - Debug, - Trace, + // 每个变体代表一种日志级别,从严重到轻微排列: + Error, // 错误:程序出现了问题,需要关注。 + Warn, // 警告:潜在的问题,不一定需要立即处理。 + Info, // 信息:一般的运行信息(最常用的级别)。 + Debug, // 调试:用于开发调试的详细信息。 + Trace, // 跟踪:非常详细的调试信息(比 Debug 更细粒度)。 + // Unknown 是一个"带数据的变体"(tuple variant),包含一个 String。 + // 当日志级别不在上述 5 种之内时,用 Unknown 保存原始的级别字符串。 + // 例如日志级别为 "CRITICAL" 时,会被解析为 Unknown("CRITICAL")。 Unknown(String), } +// ─── FromStr 实现:从字符串解析为 LogLevel ────────────────────────────────── +// `impl FromStr for LogLevel` 为 LogLevel 实现 FromStr trait。 +// 这使得可以使用 `"INFO".parse::()` 或 `str::parse::(s)` +// 将字符串解析为 LogLevel 枚举值。 +// +// FromStr trait 要求定义两个东西: +// 1. type Err — 解析失败时的错误类型。 +// 2. fn from_str(s: &str) -> Result — 解析逻辑。 impl FromStr for LogLevel { + // `type Err = std::convert::Infallible` — 错误类型设为 Infallible("不可失败的")。 + // Infallible 是一个空枚举(没有变体),表示"这个解析永远不会失败"。 + // 因为对于未知的日志级别,我们返回 Unknown(...) 而不是报错, + // 所以解析操作确实不可能失败。 type Err = std::convert::Infallible; + // `fn from_str(s: &str) -> Result` — 解析函数。 + // 接收一个字符串切片 &str,返回 Result。 + // 由于 Err 类型是 Infallible,实际上返回值总是 Ok(LogLevel)。 fn from_str(s: &str) -> Result { + // `s.to_uppercase()` — 将字符串转换为大写,实现不区分大小写的匹配。 + // 例如 "info"、"Info"、"INFO" 都会被转换为 "INFO"。 + // 返回一个新的 String(堆分配)。 + // + // `.as_str()` — 将 String 转换回 &str(字符串切片引用)。 + // 因为 match 需要匹配 &str 而不是 String。 match s.to_uppercase().as_str() { + // `|` 在 match 分支中表示"或"(multiple patterns)。 + // "ERROR" | "ERR" | "SEVERE" | "FATAL" 都匹配到 LogLevel::Error。 "ERROR" | "ERR" | "SEVERE" | "FATAL" => Ok(LogLevel::Error), "WARN" | "WARNING" | "WRN" => Ok(LogLevel::Warn), "INFO" | "INFORMATION" => Ok(LogLevel::Info), "DEBUG" | "DBG" => Ok(LogLevel::Debug), "TRACE" | "TRC" => Ok(LogLevel::Trace), + // `_` 是通配符,匹配所有未被上面分支捕获的值。 + // 对于未知级别,包装为 Unknown 并保存原始字符串。 + // s.to_string() 将 &str 转换为 String(注意这里用原始的 s,不是大写后的)。 _ => Ok(LogLevel::Unknown(s.to_string())), } } } +// ─── Display 实现:自定义 LogLevel 的显示格式 ──────────────────────────────── +// `impl fmt::Display for LogLevel` 实现 Display trait, +// 定义了用 `{}` 格式化(即 .to_string())时的输出。 impl fmt::Display for LogLevel { + // `fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result` + // - &self: 对自身的不可变引用。 + // - f: &mut fmt::Formatter 是格式化输出"写入器",用于写入输出字符串。 + // <'_> 是生命周期省略语法,表示 Formatter 借用了一个短生命周期的引用。 + // - fmt::Result: 结果类型,成功返回 Ok(()),失败返回 Err(极少见)。 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self { + // `write!(f, "ERROR")` — 向格式化写入器写入字符串 "ERROR"。 + // write! 是一个宏(注意感叹号),类似于 println!,但写入到指定的 f 而不是标准输出。 LogLevel::Error => write!(f, "ERROR"), LogLevel::Warn => write!(f, "WARN"), LogLevel::Info => write!(f, "INFO"), LogLevel::Debug => write!(f, "DEBUG"), LogLevel::Trace => write!(f, "TRACE"), + // `{s}` 是格式化占位符,将 s 的值插入到输出字符串中。 + // 例如 Unknown("custom") → "UNKNOWN(custom)"。 LogLevel::Unknown(s) => write!(f, "UNKNOWN({s})"), } } } +// ─── LogEntry 结构体 ──────────────────────────────────────────────────────── /// 一行解析后的日志 +/// +/// 表示日志文件中经过解析器处理后的一行内容。 +/// 包含原始文本、行号、时间戳、日志级别以及其他 JSON 字段。 #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] pub struct LogEntry { + // `pub` 表示字段是公开的,外部代码可以直接访问。 + // 如果不加 pub,字段只能在定义该结构体的模块内访问。 + /// 行号(从 0 开始) pub line_number: u64, + + /// 原始行内容(未经处理的完整文本) pub raw_line: String, + + /// 时间戳(如果日志中有时间字段)。Option 表示时间戳可能不存在。 + /// Some("2024-01-01T00:00:00Z") 表示有时间戳,None 表示没有。 pub timestamp: Option, + + /// 日志级别(如果日志中有级别字段)。Option 表示级别可能不存在。 pub level: Option, + + /// 其他 JSON 字段(除 timestamp 和 level 外的所有字段)。 + /// HashMap 是一个字典,键是字段名,值是 JSON 值。 + /// 例如 {"message": "hello", "request_id": "abc123"}。 pub fields: HashMap, } +// ─── SearchResult 结构体 ──────────────────────────────────────────────────── /// 搜索匹配结果 +/// +/// 记录一次搜索匹配在文件中的位置信息: +/// 在哪一行的哪个字符范围内匹配到了搜索内容。 #[derive(Debug, Clone, PartialEq)] pub struct SearchResult { + /// 匹配所在的行号 pub line_number: u64, + /// 匹配开始处的字符偏移量(从 0 开始) pub match_start: usize, + /// 匹配结束处的字符偏移量(不包含,即左闭右开区间 [match_start, match_end)) pub match_end: usize, } +// ─── SearchQuery 枚举 ────────────────────────────────────────────────────── /// 搜索查询 +/// +/// 表示用户输入的搜索内容,支持两种模式: +/// - Regex: 正则表达式搜索(模式匹配) +/// - Keyword: 关键词搜索(精确文本查找) #[derive(Debug, Clone, PartialEq)] pub enum SearchQuery { + // Regex(String) — "元组变体"(tuple variant),包含一个正则表达式字符串。 + // 例如 SearchQuery::Regex("error|warn")。 Regex(String), + // Keyword(String) — 包含一个关键词字符串。 + // 例如 SearchQuery::Keyword("connection timeout")。 Keyword(String), } +// ─── SearchFilter 结构体 ──────────────────────────────────────────────────── /// 搜索过滤器 +/// +/// 用于在搜索基础上进一步过滤结果,可以按日志级别和/或时间范围筛选。 #[derive(Debug, Clone, PartialEq)] pub struct SearchFilter { + /// 按日志级别过滤。None 表示不过滤级别;Some(vec![...]) 表示只保留这些级别。 + /// Option> 意味着这个过滤器字段是可选的。 pub levels: Option>, + + /// 按时间范围过滤。None 表示不过滤时间;Some(("起始时间", "结束时间")) 表示只保留此范围内的日志。 + /// (String, String) 是一个元组(tuple),包含起始和结束时间字符串。 pub time_range: Option<(String, String)>, } +// ─── Bookmark 结构体 ──────────────────────────────────────────────────────── /// 书签 +/// +/// 用户可以在日志中标记某一行作为书签,方便后续快速跳转。 #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] pub struct Bookmark { + /// 被标记的行号 pub line_number: u64, + /// 书签标签(用户自定义的名称)。None 表示没有标签。 pub label: Option, + /// 书签创建时间(字符串形式的时间戳,如 "2024-01-01T12:00:00") pub created_at: String, } +// ─── FileSession 结构体 ───────────────────────────────────────────────────── /// 打开的文件会话信息 +/// +/// 表示当前打开的日志文件的基本信息(元数据), +/// 用于在 UI 中显示文件名、大小、行数等信息。 #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] pub struct FileSession { + /// 文件路径 pub file_path: std::path::PathBuf, + /// 用于在 UI 中显示的文件名(通常是文件名部分,不包含目录) pub display_name: String, + /// 文件总行数 pub total_lines: u64, + /// 文件大小(字节) pub file_size: u64, } +// ─── 单元测试 ──────────────────────────────────────────────────────────────── #[cfg(test)] mod tests { use super::*; #[test] + // 测试:ERROR 级别的各种写法都能正确解析。 + // .parse::() 利用 FromStr trait 将字符串解析为 LogLevel。 + // :: 是 turbofish 语法,指定泛型参数。 fn test_from_str_error_variants() { + // 大写 "ERROR" assert_eq!("ERROR".parse::(), Ok(LogLevel::Error)); + // 小写 "error" — 不区分大小写 assert_eq!("error".parse::(), Ok(LogLevel::Error)); + // 缩写 "ERR" assert_eq!("ERR".parse::(), Ok(LogLevel::Error)); + // "SEVERE" — Java 日志中常用的严重级别 assert_eq!("SEVERE".parse::(), Ok(LogLevel::Error)); + // "FATAL" — 表示致命错误 assert_eq!("FATAL".parse::(), Ok(LogLevel::Error)); } #[test] + // 测试:WARN 级别的各种写法。 fn test_from_str_warn_variants() { assert_eq!("WARN".parse::(), Ok(LogLevel::Warn)); assert_eq!("warn".parse::(), Ok(LogLevel::Warn)); @@ -115,6 +255,7 @@ mod tests { } #[test] + // 测试:INFO 级别的各种写法。 fn test_from_str_info_variants() { assert_eq!("INFO".parse::(), Ok(LogLevel::Info)); assert_eq!("info".parse::(), Ok(LogLevel::Info)); @@ -122,6 +263,7 @@ mod tests { } #[test] + // 测试:DEBUG 级别的各种写法。 fn test_from_str_debug_variants() { assert_eq!("DEBUG".parse::(), Ok(LogLevel::Debug)); assert_eq!("debug".parse::(), Ok(LogLevel::Debug)); @@ -129,6 +271,7 @@ mod tests { } #[test] + // 测试:TRACE 级别的各种写法。 fn test_from_str_trace_variants() { assert_eq!("TRACE".parse::(), Ok(LogLevel::Trace)); assert_eq!("trace".parse::(), Ok(LogLevel::Trace)); @@ -136,20 +279,25 @@ mod tests { } #[test] + // 测试:无法识别的级别字符串应该解析为 Unknown 变体。 fn test_from_str_unknown() { assert_eq!( "FOOBAR".parse::(), + // .into() 在这里将 &str 转换为 String。 Ok(LogLevel::Unknown("FOOBAR".into())) ); } #[test] + // 测试:LogLevel 的 Display 输出格式是否正确。 fn test_display_output() { assert_eq!(LogLevel::Error.to_string(), "ERROR"); assert_eq!(LogLevel::Warn.to_string(), "WARN"); assert_eq!(LogLevel::Info.to_string(), "INFO"); assert_eq!(LogLevel::Debug.to_string(), "DEBUG"); assert_eq!(LogLevel::Trace.to_string(), "TRACE"); + // Unknown 变体的显示格式是 "UNKNOWN(原始字符串)"。 + // .into() 将 &str 转换为 String。 assert_eq!( LogLevel::Unknown("custom".into()).to_string(), "UNKNOWN(custom)" @@ -157,10 +305,14 @@ mod tests { } #[test] + // 测试:LogEntry 结构体的构造和字段访问。 fn test_log_entry_construction() { + // 创建一个 HashMap 并插入一个键值对。 let mut fields = HashMap::new(); + // .insert() 向 HashMap 中插入键值对。 fields.insert("key".to_string(), Value::String("value".to_string())); + // 使用结构体字面量语法创建 LogEntry 实例。 let entry = LogEntry { line_number: 42, raw_line: "test line".to_string(), @@ -169,10 +321,12 @@ mod tests { fields, }; + // 逐字段验证。 assert_eq!(entry.line_number, 42); assert_eq!(entry.raw_line, "test line"); assert_eq!(entry.timestamp, Some("2024-01-01T00:00:00".to_string())); assert_eq!(entry.level, Some(LogLevel::Info)); + // fields.get("key") 返回 Option<&Value>(值的引用)。 assert_eq!( entry.fields.get("key"), Some(&Value::String("value".to_string())) @@ -180,6 +334,7 @@ mod tests { } #[test] + // 测试:SearchResult 结构体的构造和字段访问。 fn test_search_result_construction() { let result = SearchResult { line_number: 10,