跳转至

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::{{…}}` 会编译失败"
            );
        }
    }
}