crates\nfarms-core\src\lib.rs¶
源码路径:
crates\nfarms-core\src\lib.rs· 150 行
//! NFarms HUB 核心逻辑层。
//!
//! 这一层是 C# 版 `src\*.cs` 里**与界面无关**的部分的等价移植,原则:
//!
//! 1. **先求等价,再求地道。** 输出、字段名、判定口径全部对齐 C# 版,
//! 包括一些看起来「怪」的细节(CRLF、`True`/`False`、JSON 带 BOM),
//! 因为验收方式是拿原版程序当 oracle 逐字节 `diff`。
//! 2. **副作用显式化。** C# 里藏在属性 getter 中的副作用(比如读一次路径就
//! 顺带做一次设置迁移),这里一律拆成独立函数由调用方决定节奏。
//! 3. **全局状态改成注入。** C# 的 `static class Paths` / `static class Log`
//! 读的是进程全局状态,不可测;这里都是可构造的值,测试能指向临时目录。
// 模块声明按模块名的字典序排(`libs < live < log`、`tools < topo` 是两组容易看反的)。
//
// 理由只有一个:找模块时不用先猜「它在不在末尾」。它对编译结果毫无影响,
// 所以新增模块直接插进对应位置就行,不存在兼容性代价。
// 旧写法把 `chat` … `ztna` 这六个单独排在末尾一段,同一个列表里并存两种顺序,
// 读的人得来回扫两遍才敢确定某个模块不存在。
// (`tests::module_declarations_are_sorted` 守着这条顺序。)
pub mod browser;
pub mod chat;
pub mod cyinfo;
pub mod dsh;
pub mod envprobe;
pub mod ffmpeg;
// Windows Job Object 的最小封装(`live.rs` 与 `observability.rs` **共用**这一份,
// 见模块头)。非 Windows 上不存在 —— Android 那边没有 Job Object 这个东西,
// 留着空壳只会让"设了其实没设"看起来像设了。
#[cfg(windows)]
pub(crate) mod job_win;
pub mod kb;
pub mod libs;
pub mod live;
pub mod log;
pub mod music;
pub mod netclock;
pub mod observability;
pub mod paths;
pub mod remote;
pub mod scripts;
pub mod settings;
pub mod store;
pub mod time;
pub mod today;
pub mod tools;
pub mod topo;
pub mod videoparse;
pub mod weather;
pub mod wireshark;
pub mod ztna;
// 只把 `Log` / `Paths` / `Settings` 这三样提到 crate 门口,其余模块一律走全路径
// (`nfarms_core::weather::parse` 这种)。
//
// 挑选标准是**三个下游 crate 都要用**:`nfarms-app`、`nfarms-gui`、`nfarms-mobile`
// 的 `use nfarms_core::{…}` 里都点了这三个名。多提一个就多一份「该用门口那个还是
// 模块里那个」的歧义;反过来少提一个,三个下游的 `use` 都得改回全路径。
pub use log::Log;
pub use paths::Paths;
pub use settings::Settings;
/// 把 Rust 的 `bool` 按 .NET 的 `bool.ToString()` 输出。
///
/// `Console.WriteLine(ok)` 在 .NET 里打的是 `True` / `False`,
/// Rust 的 `Display` 打的是 `true` / `false` —— 这一处不抹平,
/// 所有自检输出的比对都会失败。集中放这里,省得每个命令各写一遍。
#[inline]
pub fn csharp_bool(v: bool) -> &'static str {
if v { "True" } else { "False" }
}
/// 去掉 UTF-8 BOM。
///
/// C# 的 `File.ReadAllText(path, Encoding.UTF8)` 默认开启 BOM 探测并吞掉 BOM;
/// Rust 的 `read_to_string` 会把 BOM 原样留在字符串开头。而本项目的
/// `settings.json` 恰恰是**带 BOM** 写的(`new UTF8Encoding(true)`),
/// 不处理的话 `serde_json` 会直接报 "expected value"。
#[inline]
pub fn strip_bom(s: &str) -> &str {
s.strip_prefix('\u{feff}').unwrap_or(s)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn csharp_bool_matches_dotnet_spelling() {
assert_eq!(csharp_bool(true), "True");
assert_eq!(csharp_bool(false), "False");
}
#[test]
fn strip_bom_only_touches_leading_bom() {
assert_eq!(strip_bom("\u{feff}{\"a\":1}"), "{\"a\":1}");
assert_eq!(strip_bom("{\"a\":1}"), "{\"a\":1}");
}
/// `pub mod` 必须保持字典序(见声明处的注释)。
///
/// 顺序纯属排列,不影响编译:谁把新模块追加到末尾,编译器、clippy、下游 crate
/// 都不会有任何反应 —— 只有读代码的人会多扫一遍。所以这里直接读本文件源码,
/// 把顺序错误变成一个会红的测试。
#[test]
fn module_declarations_are_sorted() {
let src = include_str!("lib.rs");
let declared: Vec<&str> = src
.lines()
.filter_map(|line| line.strip_prefix("pub mod "))
.filter_map(|line| line.strip_suffix(';'))
.collect();
// 先确认解析真的抓到了声明:否则把 `pub mod` 改成别的写法(例如 `pub(crate) mod`)
// 会让本测试在空列表上「排序正确」地通过,等于悄悄失效。
assert!(
declared.contains(&"browser") && declared.contains(&"ztna"),
"没解析到 pub mod 声明,排序检查已失效: {declared:?}"
);
let mut sorted = declared.clone();
sorted.sort_unstable();
assert_eq!(declared, sorted, "pub mod 要按字典序排(见声明处注释)");
}
/// 三个 `pub use` 是给下游 crate 的契约:`nfarms-app` / `nfarms-gui` / `nfarms-mobile`
/// 都写 `use nfarms_core::{Log, Paths, Settings}`。
///
/// `nfarms-core` 自己并不引用这三个名字,所以删掉一行本 crate 照样编译通过,
/// 只有下游 crate 才会炸 —— 而单元测试只编本 crate,看不到那一步。
#[test]
fn the_three_root_reexports_stay_available() {
// 类型层面的钉住:改名或换模块路径会在这里直接编译失败。
let _: Option<Log> = None;
let _: Option<Paths> = None;
let _: Option<Settings> = None;
let src = include_str!("lib.rs");
for decl in [
"pub use log::Log;",
"pub use paths::Paths;",
"pub use settings::Settings;",
] {
assert!(
src.contains(decl),
"根上的 `{decl}` 没了,三个下游 crate 的 `use nfarms_core::{{…}}` 会编译失败"
);
}
}
}