diff --git a/Cargo.toml b/Cargo.toml index 2a4982b..8b4a721 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -59,7 +59,10 @@ features = [] # ! 【2024-03-21 09:24:51】暂时没有特性 [dependencies.babel_nar] version = "0.25" -features = ["bundled"] +features = [ # * 🚩【2024-09-12 18:04:39】目前锁定以下两个特性 + "cin_implements", # 各大CIN的NAVM实现 + "test_tools", # 测试工具集 +] ## 依赖特性的可选依赖 ## diff --git a/src/arg_parse.rs b/src/cli/arg_parse.rs similarity index 98% rename from src/arg_parse.rs rename to src/cli/arg_parse.rs index 445b463..95c1604 100644 --- a/src/arg_parse.rs +++ b/src/cli/arg_parse.rs @@ -2,7 +2,7 @@ //! * ⚠️【2024-04-01 14:31:09】特定于二进制crate,目前不要并入[`babel_nar`] //! * 🚩【2024-04-04 03:03:58】现在移出所有与「启动配置」相关的逻辑到[`super::vm_config`] -use crate::{load_config_extern, read_config_extern, LaunchConfig}; +use crate::cli::{load_config_extern, read_config_extern, LaunchConfig}; use babel_nar::println_cli; use clap::Parser; use std::{ @@ -203,8 +203,8 @@ mod tests { /// 测试/加载配置 mod read_config { use super::*; - use crate::vm_config::*; - use crate::LaunchConfigWebsocket; + use crate::cli::vm_config::*; + use crate::cli::LaunchConfigWebsocket; use config_paths::*; use nar_dev_utils::manipulate; diff --git a/src/config_launcher.rs b/src/cli/config_launcher.rs similarity index 99% rename from src/config_launcher.rs rename to src/cli/config_launcher.rs index d47876c..4938530 100644 --- a/src/config_launcher.rs +++ b/src/cli/config_launcher.rs @@ -1,6 +1,6 @@ //! 用于从「启动参数」启动NAVM运行时 -use crate::{ +use crate::cli::{ read_config_extern, search_configs, LaunchConfig, LaunchConfigCommand, LaunchConfigTranslators, RuntimeConfig, SUPPORTED_CONFIG_EXTENSIONS, }; diff --git a/src/config_search.rs b/src/cli/config_search.rs similarity index 98% rename from src/config_search.rs rename to src/cli/config_search.rs index 077f0fa..f953251 100644 --- a/src/config_search.rs +++ b/src/cli/config_search.rs @@ -1,6 +1,6 @@ //! CIN自动搜索 -use crate::{read_config_extern, LaunchConfig}; +use crate::cli::{read_config_extern, LaunchConfig}; use anyhow::Result; use babel_nar::{ cli_support::cin_search::{name_match::is_name_match, path_walker::PathWalkerV1}, diff --git a/src/cli/mod.rs b/src/cli/mod.rs new file mode 100644 index 0000000..5f2a9fe --- /dev/null +++ b/src/cli/mod.rs @@ -0,0 +1,17 @@ +//! 原BabelNAR.rs `src/bin/babelnar_cli/*.rs` +//! * 🚩【2024-09-12 17:41:35】现在统一放置在`src/cli`下 + +nar_dev_utils::mods! { + // 启动参数 + pub use vm_config; + // 命令行解析 + pub use arg_parse; + // 配置(自动)搜索 + pub use config_search; + // 从配置启动 + pub use config_launcher; + // 运行时交互、管理 + pub use runtime_manage; + // Websocket服务端 + pub use websocket_server; +} diff --git a/src/runtime_manage.rs b/src/cli/runtime_manage.rs similarity index 99% rename from src/runtime_manage.rs rename to src/cli/runtime_manage.rs index 655cca2..e50db7d 100644 --- a/src/runtime_manage.rs +++ b/src/cli/runtime_manage.rs @@ -1,7 +1,7 @@ //! 启动后运行时的(交互与)管理 use super::websocket_server::*; -use crate::{launch_by_runtime_config, InputMode, LaunchConfigPreludeNAL, RuntimeConfig}; +use crate::cli::{launch_by_runtime_config, InputMode, LaunchConfigPreludeNAL, RuntimeConfig}; use anyhow::{anyhow, Result}; use babel_nar::{ cli_support::{ diff --git a/src/vm_config.rs b/src/cli/vm_config.rs similarity index 100% rename from src/vm_config.rs rename to src/cli/vm_config.rs diff --git a/src/websocket_server.rs b/src/cli/websocket_server.rs similarity index 99% rename from src/websocket_server.rs rename to src/cli/websocket_server.rs index 70ade4a..cc5da08 100644 --- a/src/websocket_server.rs +++ b/src/cli/websocket_server.rs @@ -2,7 +2,7 @@ //! * 🎯为BabelNAR CLI实现Websocket IO //! * 🎯实现专有的Websocket服务端逻辑 -use crate::{LaunchConfigWebsocket, RuntimeConfig, RuntimeManager}; +use crate::cli::{LaunchConfigWebsocket, RuntimeConfig, RuntimeManager}; use anyhow::Result; use babel_nar::{ cli_support::{ diff --git a/src/lib.rs b/src/lib.rs new file mode 100644 index 0000000..b2927a4 --- /dev/null +++ b/src/lib.rs @@ -0,0 +1,9 @@ +//! BabelNAR-CLI.rs 功能模块 + +// CLI支持 +// * ✨终端IO +// * ✨路径查找 +pub mod support; + +// CLI主程序功能 +pub mod cli; diff --git a/src/main.rs b/src/main.rs index 54448a1..5693d15 100644 --- a/src/main.rs +++ b/src/main.rs @@ -9,26 +9,9 @@ use anyhow::Result; use babel_nar::{eprintln_cli, println_cli}; +use babel_nar_cli::cli::*; use clap::Parser; -use std::io::Result as IoResult; -use std::thread::sleep; -use std::time::Duration; -use std::{env, path::PathBuf}; - -nar_dev_utils::mods! { - // 启动参数 - use vm_config; - // 命令行解析 - use arg_parse; - // 配置(自动)搜索 - use config_search; - // 从配置启动 - use config_launcher; - // 运行时交互、管理 - use runtime_manage; - // Websocket服务端 - use websocket_server; -} +use std::{env, io::Result as IoResult, path::PathBuf, thread::sleep, time::Duration}; /// 主入口 pub fn main() -> Result<()> { diff --git a/src/support/cin_search/anyhow_vm.rs b/src/support/cin_search/anyhow_vm.rs new file mode 100644 index 0000000..1150271 --- /dev/null +++ b/src/support/cin_search/anyhow_vm.rs @@ -0,0 +1,125 @@ +//! 🚩【2024-03-30 23:36:48】曾经的尝试: +//! * 所有「路径构建器」都返回一个动态的「虚拟机启动器」类型 +//! * 启动时只需在一个「Anyhow虚拟机启动器」列表中选择 + +trait Turned { + fn say(&self); +} +trait Unturned { + type Target: Turned; + fn turn(self) -> Self::Target; + fn turn_box(self: Box) -> Box; + fn turn_box_sized(self: Box) -> Box + where + Self: Sized, + { + Box::new(self.turn()) + } +} +struct U(usize); +struct T(usize); +impl Turned for T { + fn say(&self) { + print!("I'm T({})", self.0) + } +} +impl Unturned for U { + type Target = T; + fn turn(self) -> T { + T(self.0) + } + fn turn_box(self: Box) -> Box { + self.turn_box_sized() + } +} +struct AnyhowUnturned { + inner: Box>, +} +struct AnyhowTurned { + inner: Box, +} +impl Turned for AnyhowTurned { + fn say(&self) { + self.inner.say() + } +} +impl Unturned for AnyhowUnturned { + type Target = AnyhowTurned; + fn turn(self) -> AnyhowTurned { + AnyhowTurned { + inner: self.inner.turn_box(), + } + } + + fn turn_box(self: Box) -> Box { + self.turn_box_sized() + } +} +impl> From for AnyhowUnturned { + fn from(value: U) -> Self { + Self { + inner: Box::new(value), + } + } +} +struct AnyhowUnturned2 { + inner: AnyhowTurned, +} + +fn main() { + let unturned: AnyhowUnturned<_> = U(1).into(); +} + +// pub struct AnyhowLauncher<'a, Runtime: VmRuntime + 'a> { +// pub launcher: Box + 'a>, +// } + +// impl<'a, Runtime: VmRuntime + 'a> AnyhowLauncher<'a, Runtime> { +// pub fn new(launcher: impl VmLauncher + 'a) -> Self +// where +// Launcher: VmLauncher + 'a, +// { +// Self { +// launcher: Box::new(launcher), +// } +// } +// } + +// /// ! Box不能充当`VmLauncher`的参数:未实现`VmRuntime` +// impl<'a, Runtime: VmRuntime + 'a> VmLauncher> for AnyhowLauncher<'a, Runtime> { +// fn launch(self) -> AnyhowRuntime<'a> { +// AnyhowRuntime { +// inner: Box::new(self.launcher.launch()), +// } +// } +// } + +// struct AnyhowRuntime<'a> { +// inner: Box, +// } + +// impl AnyhowRuntime<'_> { +// fn new(inner: impl VmRuntime) -> Self { +// Self { +// inner: Box::new(inner), +// } +// } +// } + +// impl VmRuntime for AnyhowRuntime<'_> { +// fn input_cmd(&mut self, cmd: navm::cmd::Cmd) -> anyhow::Result<()> { +// self.inner.input_cmd(cmd) +// } + +// fn fetch_output(&mut self) -> anyhow::Result { +// self.inner.fetch_output() +// } + +// fn try_fetch_output(&mut self) -> anyhow::Result> { +// self.inner.try_fetch_output() +// } + +// fn terminate(self) -> anyhow::Result<()> { +// self.inner.terminate() +// } +// } diff --git a/src/support/cin_search/impls_path_builder/mod.rs b/src/support/cin_search/impls_path_builder/mod.rs new file mode 100644 index 0000000..fea5938 --- /dev/null +++ b/src/support/cin_search/impls_path_builder/mod.rs @@ -0,0 +1,74 @@ +//! 存储各CIN的「路径构建器」 +//! * ✅OpenNARS +//! * ✅ONA +//! TODO: PyNARS +//! TODO: CXinNARS +//! * 🚩【2024-03-31 01:27:09】其它接口完成度不高的CIN,暂时弃了 + +use crate::support::cin_search::{ + name_match::is_name_match, path_builder::CinPathBuilder, path_walker::PathWalker, +}; +use navm::vm::{VmLauncher, VmRuntime}; +use std::path::Path; + +nar_dev_utils::mods! { + // OpenNARS + use pub path_builder_opennars; + // ONA + use pub path_builder_ona; +} + +// 深入条件 +pub fn file_name_matches(path: &Path, name: &str) -> bool { + path.file_name().is_some_and(|name_os| { + name_os + .to_str() + .is_some_and(|name_str| is_name_match(name, name_str)) + }) +} + +/// 从遍历者中找到匹配的所有启动器 +/// * 🎯仅搜索出「可能有效,故构建好」的启动器 +pub fn launchers_from_walker( + path_walker: impl PathWalker, + path_builder: impl CinPathBuilder, +) -> Vec<(L, usize)> { + path_walker + .to_iter_fn() + .filter_map(Result::ok) + .filter_map(|p| path_builder.try_construct_from_path(&p)) + .collect::>() +} + +/// 类似[`launchers_from_walker`],但根据返回的「匹配度」从高到底排序 +pub fn launchers_from_walker_sorted( + path_walker: impl PathWalker, + path_builder: impl CinPathBuilder, +) -> Vec { + // 获取 & 排序 + let mut launchers = launchers_from_walker(path_walker, path_builder); + launchers.sort_by(|(_, a), (_, b)| b.cmp(a)); // ←此处是倒序 + // 提取左侧元素 + launchers.into_iter().map(|(l, _)| l).collect::>() +} + +/// 单元测试 +#[cfg(test)] +mod tests { + use super::*; + use crate::support::cin_search::path_walker::PathWalkerV1; + use std::env::current_dir; + + #[test] + fn test() { + let path_walker = PathWalkerV1::new(¤t_dir().unwrap(), |path| { + file_name_matches(path, "nars") + }) + .unwrap(); + + dbg!(launchers_from_walker_sorted( + path_walker, + PathBuilderOpenNARS + )); + } +} diff --git a/src/support/cin_search/impls_path_builder/path_builder_ona.rs b/src/support/cin_search/impls_path_builder/path_builder_ona.rs new file mode 100644 index 0000000..f803abe --- /dev/null +++ b/src/support/cin_search/impls_path_builder/path_builder_ona.rs @@ -0,0 +1,85 @@ +//! 用于ONA的路径构建器 + +use crate::support::cin_search::{ + name_match::{name_match, name_match_only_contains}, + path_builder::CinPathBuilder, +}; +use babel_nar::{cin_implements::ona::ONA, runtimes::CommandVmRuntime}; +use nar_dev_utils::{if_return, OptionBoost}; +use std::path::Path; + +/// ONA路径构建器 +/// * 🎯判别路径并构建ONA启动器 +pub struct PathBuilderONA; + +impl PathBuilderONA { + // 匹配文件名 + #[inline(always)] + fn match_name(name: &str) -> usize { + // 常用的`NAR.exe` + (if name == "NAR.exe" { 10 } else { 0 }) + // 综合,只需「均不满足⇒0」即可 + + name_match("ona", name) + + name_match_only_contains("opennars-for-application", name) + + name_match_only_contains("opennars_for_application", name) + } + + /// 检查文件匹配度 + fn valid_exe(path: &Path) -> usize { + // ! 不一定是本地存在的文件 + if_return! { !path.extension().is_some_and(|ex| ex == "exe") => 0} + // 名称匹配`ona` + path.file_name().map_unwrap_or( + |name_os| name_os.to_str().map_unwrap_or(Self::match_name, 0), + 0, + ) + } +} + +impl CinPathBuilder for PathBuilderONA { + type Runtime = CommandVmRuntime; + type Launcher = ONA; + + fn match_path(&self, path: &Path) -> usize { + // ! 与本地文件系统有关 + // 不是本地的文件⇒0 + if_return! { !path.is_file() => 0 } + // 否则⇒查看exe匹配度 + Self::valid_exe(path) + } + + fn construct_from_path(&self, path: &Path) -> Self::Launcher { + ONA::new(path) + } +} + +/// 单元测试 +#[cfg(test)] +mod tests { + use super::*; + use nar_dev_utils::{f_parallel, fail_tests}; + use std::path::Path; + + /// 工具/测试单个路径 + fn test_matched(path: &str) { + let path = Path::new(path); + assert!(dbg!(PathBuilderONA::valid_exe(path)) > 0); + } + + /// 测试/名称匹配 + #[test] + fn test_match() { + f_parallel![ + test_matched; + "../NAR.exe"; + "../opennars-for-applications.exe"; + "../ona.exe"; + "ona_old.exe"; + ]; + } + + fail_tests! { + 无效扩展名 test_matched("../opennars.exe"); + 无效名称 test_matched("../NARust.exe"); + } +} diff --git a/src/support/cin_search/impls_path_builder/path_builder_opennars.rs b/src/support/cin_search/impls_path_builder/path_builder_opennars.rs new file mode 100644 index 0000000..07a2123 --- /dev/null +++ b/src/support/cin_search/impls_path_builder/path_builder_opennars.rs @@ -0,0 +1,79 @@ +//! 用于OpenNARS的路径构建器 + +use crate::support::cin_search::{name_match::name_match, path_builder::CinPathBuilder}; +use babel_nar::{cin_implements::opennars::OpenNARS, runtimes::CommandVmRuntime}; +use nar_dev_utils::{if_return, OptionBoost}; +use std::path::Path; + +/// OpenNARS路径构建器 +/// * 🎯判别路径并构建OpenNARS启动器 +pub struct PathBuilderOpenNARS; + +impl PathBuilderOpenNARS { + // 匹配文件名 + #[inline(always)] + fn match_name(name: &str) -> usize { + // 二者综合,只需「二者均不满足⇒0」即可 + name_match("opennars", name) + name_match("open_nars", name) + } + + /// 检查文件匹配度 + fn valid_jar(path: &Path) -> usize { + // ! 不一定是本地存在的文件 + if_return! { !path.extension().is_some_and(|ex| ex == "jar") => 0} + // 名称匹配`opennars` + path.file_name().map_unwrap_or( + |name_os| name_os.to_str().map_unwrap_or(Self::match_name, 0), + 0, + ) + } +} + +impl CinPathBuilder for PathBuilderOpenNARS { + type Runtime = CommandVmRuntime; + type Launcher = OpenNARS; + + fn match_path(&self, path: &Path) -> usize { + // ! 与本地文件系统有关 + // 不是本地的文件⇒0 + if_return! { !path.is_file() => 0 } + // 否则⇒查看jar匹配度 + Self::valid_jar(path) + } + + fn construct_from_path(&self, path: &Path) -> Self::Launcher { + OpenNARS::new(path) + } +} + +/// 单元测试 +#[cfg(test)] +mod tests { + use super::*; + use nar_dev_utils::{f_parallel, fail_tests}; + use std::path::Path; + + /// 工具/测试单个路径 + fn test_matched(path: &str) { + let path = Path::new(path); + assert!(dbg!(PathBuilderOpenNARS::valid_jar(path)) > 0); + } + + /// 测试/名称匹配 + #[test] + fn test_match() { + f_parallel![ + test_matched; + "../opennars-304-T-modified.jar"; + "../OpenNARS-3.0.4-Snapshot.jar"; + "../opennars.jar"; + "open_nars.jar"; + "opennars-3.0.4-SNAPSHOT.jar"; + ]; + } + + fail_tests! { + 无效扩展名 test_matched("../opennars-304-T-modified.jar.exe"); + 无效名称 test_matched("../ona-T-modified.jar"); + } +} diff --git a/src/support/cin_search/mod.rs b/src/support/cin_search/mod.rs new file mode 100644 index 0000000..bf53893 --- /dev/null +++ b/src/support/cin_search/mod.rs @@ -0,0 +1,22 @@ +//! CIN启动器中有关「CIN路径构建(搜索)」的逻辑 +//! * ✨根据「CIN路径构建器」搜索(判别)系统中已存在的CIN实现(并自动构建) +//! * 🚩输入:搜索起点(一般是编译后exe所在文件夹) +//! * 🚩输出:NAVM启动器列表 +//! * ❓【2024-03-30 19:12:29】是否要考虑返回更细化的「CIN实例位置」而非「CIN启动器」,以避免额外的性能开销? + +// 导出模块 +nar_dev_utils::mods! { + // anyhow | 弃用 + // anyhow_vm; + // 名称匹配 + pub name_match; + + // 路径遍历器 + pub path_walker; + + // 路径构建器 + pub path_builder; + + // 路径构建器的各CIN实现 + pub impls_path_builder; +} diff --git a/src/support/cin_search/name_match.rs b/src/support/cin_search/name_match.rs new file mode 100644 index 0000000..e6a0341 --- /dev/null +++ b/src/support/cin_search/name_match.rs @@ -0,0 +1,79 @@ +//! 封装「名称匹配」逻辑 +//! * 🎯用于「语义化」「模糊化」的字符串匹配 +//! * ✨无视大小写匹配 +//! * 📄"opennars"匹配"OpenNARS" +//! * ✨「含于」与「包含」匹配 +//! * 📄"opennars"匹配"OpenNARS 3.0.4"(含于)与"nars"(包含) +//! * ✨返回一个「匹配度」的数值 +//! * `0`统一表示「未匹配」 +//! * 剩余值可用于排序 + +use nar_dev_utils::{first, if_return}; + +/// 名称匹配 +/// * 🎯用于「语义化」「模糊化」的字符串匹配 +/// * ✨无视大小写匹配 +/// * 📄"opennars"匹配"OpenNARS" +/// * ✨「含于」与「包含」匹配 +/// * 📄"opennars"匹配"OpenNARS 3.0.4"(含于)与"nars"(包含) +/// * ⚙️返回一个「匹配度」的数值 +/// * `0`统一表示「未匹配」 +/// * 剩余值可用于排序 +pub fn name_match(name: &str, target: &str) -> usize { + // 完全相等⇒最高级 + if_return! { + // 完全相等⇒高 + name == target => 6 + // 包含于⇒中 + target.contains(name) => 4 + // 包含⇒低 + name.contains(target) => 2 + } + + // 忽略大小写的情况 | 忽略大小写,降一个匹配度 + let name = name.to_lowercase(); + let target = target.to_lowercase(); + + first! { + // 完全相等⇒高 + name == target => 5, + // 包含于⇒中 + target.contains(&name) => 3, + // 包含⇒低 + name.contains(&target) => 1, + // 否则⇒不匹配 + _ => 0, + } +} + +/// 名称匹配/仅「含于」 +/// * 🚩与[`name_match`]类似,但仅「含于」而不适配「包含」 +/// * 🎯用于「长串名称作为内部关键词」的匹配 +pub fn name_match_only_contains(name: &str, target: &str) -> usize { + // 完全相等⇒最高级 + if_return! { + // 完全相等⇒高 + name == target => 4 + // 含于⇒低 + target.contains(name) => 2 + } + + // 忽略大小写的情况 | 忽略大小写,降一个匹配度 + let name = name.to_lowercase(); + let target = target.to_lowercase(); + + first! { + // 完全相等⇒高 + name == target => 3, + // 含于⇒低 + target.contains(&name) => 1, + // 否则⇒不匹配 + _ => 0, + } +} + +/// 判断「是否匹配」,不管「匹配度」多少 +/// * 🚩直接复用逻辑,以牺牲一定性能为代价 +pub fn is_name_match(name: &str, target: &str) -> bool { + name_match(name, target) > 0 +} diff --git a/src/support/cin_search/path_builder.rs b/src/support/cin_search/path_builder.rs new file mode 100644 index 0000000..c32f5b9 --- /dev/null +++ b/src/support/cin_search/path_builder.rs @@ -0,0 +1,66 @@ +//! 统一的「路径构建器」逻辑 + +use navm::vm::{VmLauncher, VmRuntime}; +use std::path::Path; + +/// CIN路径构建器 +/// * 🚩本身不承担「遍历路径」的任务,只负责 +/// * 📌判断是否「可以用于构建NAVM运行时」 +/// * 📌从某路径构建「NAVM启动器」 +/// * ❌【2024-03-30 19:05:48】放弃「通用启动器/通用运行时」的适配尝试 +/// * 📝目前[`CinSearch::Launcher`]总是要带上[`CinSearch::Runtime`]作类型参数 +/// * 📌堵点:难以定义一个使用`Box>`封装的`AnyhowVmLauncher`类型 +/// * 📍问题领域:特征对象及其转换 +/// * ❓一个可能的参考:[`anyhow`]对「错误类型」的统一 +/// * ❌【2024-03-30 21:24:10】尝试仍然失败:有关`Box`的所有权转换问题 +/// * 🔗技术参考1: +pub trait CinPathBuilder { + /// 搜索结果的启动器类型 + /// * 📌启动后变为[`CinSearch::Runtime`]运行时类型 + type Launcher: VmLauncher; + + /// 搜索结果的运行时类型 + type Runtime: VmRuntime; + + /// 路径匹配 + /// * 🎯匹配某路径(可能是文件夹,也可能是文件)是否可用于「构建NAVM启动器」 + /// * ⚠️与**该路径是否存在**有关 + /// * 📌需要访问本地文件系统 + /// * 📄一些CIN可能要求判断其子目录的文件(附属文件) + /// * ⚙️返回「匹配度」 + /// * 📌`0`⇒不匹配,其它⇒不同程度的匹配 + /// * 🎯对接「名称匹配」中的「匹配度」 + /// * ✨可用于后续排序 + fn match_path(&self, path: &Path) -> usize; + + /// 用于检查路径是否匹配 + /// * 🔗参见[`match_path`] + fn is_path_matched(&self, path: &Path) -> bool { + self.match_path(path) > 0 + } + + /// 路径构建 + /// * 🎯从某个路径构建出一个NAVM启动器 + /// * ✅除路径以外,其它参数可作默认 + /// * 📄OpenNARS的「Java最大堆大小」 + /// + /// # Panics + /// + /// ⚠️需要保证[`is_path_matched`]为真 + /// * 为假时可能`panic` + fn construct_from_path(&self, path: &Path) -> Self::Launcher; + + /// 尝试路径构建 + /// * 🚩返回一个[`Option`] + /// * 能构建⇒返回构建后的结果 `Some((启动器, 匹配度))` + /// * 无法构建⇒返回[`None`] + #[inline] + fn try_construct_from_path(&self, path: &Path) -> Option<(Self::Launcher, usize)> { + match self.match_path(path) { + // 不匹配⇒无 + 0 => None, + // 匹配⇒元组 + n => Some((self.construct_from_path(path), n)), + } + } +} diff --git a/src/support/cin_search/path_walker.rs b/src/support/cin_search/path_walker.rs new file mode 100644 index 0000000..464697b --- /dev/null +++ b/src/support/cin_search/path_walker.rs @@ -0,0 +1,240 @@ +//! 路径遍历器 +//! * 🎯用于分离「路径查找」与「CIN识别」两功能 +//! * 📌「路径遍历器」负责「提供路径,并有选择地 深入/跳出 路径」 + +use anyhow::{Error, Result}; +use std::path::{Path, PathBuf}; + +/// 抽象的「路径遍历」特征 +/// * ✨允许「迭代出下一个路径」 +/// * 🏗️后续可能会添加更多特性,如「根据结果调整遍历策略」等 +pub trait PathWalker { + /// ✨返回「下一个路径」 + /// * 可能为空,也可能返回错误 + fn next_path(&mut self) -> Result>; + + /// 类似迭代器的`next`方法 + /// * 🎯对标`Iterator>` + /// * 🚩【2024-03-31 01:03:04】是「没法为`impl PathWalker`自动实现`Iterator`」的补偿 + fn iter_next_path(&mut self) -> Option> { + match self.next_path() { + // 正常情况 + Ok(Some(path)) => Some(Ok(path)), + // 中途报错⇒返回错误 + Err(e) => Some(Err(e)), + // 终止⇒真正终止 + Ok(None) => None, + } + } + + /// 利用[`std::iter::from_fn`]将自身转换为迭代器,而无需实现[`Iterator`]特征 + /// * 🎯便于在`impl PathWalker`中使用 + #[inline] + fn to_iter_fn<'a>(mut self) -> impl Iterator> + 'a + where + Self: Sized + 'a, + { + std::iter::from_fn(move || self.iter_next_path()) + } +} + +/// 初代路径遍历器 +/// * ✨使用「渐近回退性扫描」机制,总体为「深度优先」 +/// * 📌「起始目录」一般为exe所在目录 +/// * 🚩从「起始目录」开始,扫描其下子目录 +/// * 递归深入、迭代出文件夹与文件 +/// * 🚩若「起始目录」已扫描完毕,向上「条件扫描」父目录 +/// * 遍历其【直接包含】的文件/文件夹 +/// * 若有满足特定「可深入条件」的文件夹,则深入扫描该文件夹(仍然是「条件扫描」) +/// * 🚩父目录扫描完毕后,继续扫描父目录 +pub struct PathWalkerV1<'a> { + // 父目录堆栈 + ancestors_stack: Vec, + + /// 待遍历目录的堆栈 + to_visit_stack: Vec, + + /// 可深入条件 + deep_criterion: Box bool + Send + Sync + 'a>, + + /// 当前在遍历目录的迭代器 + current_dir_iter: Box>>, +} + +impl<'a> PathWalkerV1<'a> { + pub fn new( + start: &Path, + deep_criterion: impl Fn(&Path) -> bool + Send + Sync + 'a, + ) -> Result { + // 计算根目录 + // * 🚩不是文件夹⇒向上寻找根目录 + let mut root = start; + while !root.is_dir() { + root = root.parent().unwrap(); + } + // 构造路径堆栈 + let mut ancestors_stack = root.ancestors().map(Path::to_owned).collect::>(); + ancestors_stack.reverse(); // 从「当前→根」转为「根→当前」,先遍历当前,再遍历根 + // 拿出目录 + let root = match ancestors_stack.pop() { + Some(path) => path, + None => return Err(Error::msg("起始目录无效")), + }; + let deep_criterion = Box::new(deep_criterion); + let current_dir_iter = Box::new(Self::new_path_iter(&root)?); + Ok(Self { + ancestors_stack, + to_visit_stack: vec![], // 空栈初始化 + deep_criterion, + current_dir_iter, + }) + } + + /// ✨构造路径迭代器 + /// * 🎯尽可能让异常变得可处理:避免`unwrap` + fn new_path_iter(path: &Path) -> Result>> { + Ok(std::fs::read_dir(path)?.map(|e| match e { + Ok(entry) => Ok(entry.path()), + Err(e) => Err(e.into()), + })) + } + + /// 可能返回[`None`]的[`Self::next`] + /// * 🎯应对「切换到父目录的迭代器后,首个迭代结果还是[`None`]」的情况 + /// * 🚩解决方案:再次[`Self::poll_path`] + fn poll_path(&mut self) -> PathPollResult { + // ! ❌【2024-03-30 22:34:04】目前没法稳定地使用`?` + match self.current_dir_iter.next() { + // 正常情况 + Some(Ok(path)) => { + // 如果「值得深入」⇒预备在后续深入 + if path.is_dir() && (self.deep_criterion)(&path) { + self.to_visit_stack.push(path.clone()) + } + // 返回 + PathPollResult::Some(path) + } + // 中途报错情况 + Some(Err(e)) => PathPollResult::Err(e), + // 没有⇒尝试切换路径 + None => self.try_switch_current_path(), + } + } + + /// 尝试切换路径 + /// * 切换到一个新的路径 + fn try_switch_current_path(&mut self) -> PathPollResult { + match self.to_visit_stack.pop() { + // 「待检查路径」有⇒尝试pop一个,构造并切换到新的迭代器 + Some(path) => match self.change_current_path(&path) { + Ok(()) => PathPollResult::None, // 构造了就收手,无需立马查看里边有无路径 + Err(e) => PathPollResult::Err(e), + }, + // 「待检查路径」没有⇒尝试从「祖先路径」中尝试pop一个 + None => match self.ancestors_stack.pop() { + // 「祖先路径」有⇒尝试pop一个,构造并切换到新的迭代器 + Some(path) => match self.change_current_path(&path) { + Ok(()) => PathPollResult::None, // 构造了就收手,无需立马查看里边有无路径 + Err(e) => PathPollResult::Err(e), + }, // 「祖先路径」没有⇒终止 + None => PathPollResult::Ended, + }, + } + } + + /// 尝试更改到某个目录(的迭代器) + fn change_current_path(&mut self, path: &Path) -> Result<()> { + let iter = Self::new_path_iter(path)?; + self.current_dir_iter = Box::new(iter); + Ok(()) + } +} + +/// 枚举「路径遍历」结果 +/// * 🎯用于「路径遍历器」的返回值 +pub enum PathPollResult { + /// 拿到了一个路径 + Some(PathBuf), + /// 尝试拿,但没拿到路径 + None, + /// 尝试拿,但发生错误 + Err(Error), + /// 结束了 + Ended, +} + +impl From> for PathPollResult { + fn from(value: Option) -> Self { + match value { + Some(path) => Self::Some(path), + None => Self::None, + } + } +} + +impl From> for PathPollResult { + fn from(value: Result) -> Self { + match value { + Ok(path) => Self::Some(path), + Err(e) => Self::Err(e), + } + } +} + +impl PathWalker for PathWalkerV1<'_> { + fn next_path(&mut self) -> Result> { + // 持续不断poll自身,压缩掉其中的`None`项 + loop { + match self.poll_path() { + // 正常返回路径 + PathPollResult::Some(path) => break Ok(Some(path)), + // 没有⇒继续循环(压缩掉) + PathPollResult::None => continue, + // 报错⇒返回错误 + PathPollResult::Err(e) => break Err(e), + // 终止⇒返回终止信号 + PathPollResult::Ended => break Ok(None), + } + } + } +} + +/// 实现迭代器,返回所有「搜索结果」 +impl Iterator for PathWalkerV1<'_> { + type Item = Result; + fn next(&mut self) -> Option> { + self.iter_next_path() + } +} + +/// 单元测试 +#[cfg(test)] +mod tests { + use super::*; + use crate::support::cin_search::name_match::is_name_match; + use std::env::current_dir; + + fn _test_path_walker_v1(start: impl Into) { + // 起始目录 + let start = &start.into(); + // 深入条件 + fn deep_criterion(path: &Path) -> bool { + path.file_name() + .is_some_and(|name| name.to_str().is_some_and(|s| is_name_match("nars", s))) + } + // 构建遍历者,加上条件 + let walker = PathWalkerV1::new(start, deep_criterion).unwrap(); + // 打印遍历者的「祖先列表」 + println!("{:?}", walker.ancestors_stack); + // 遍历 + for path in walker { + println!("{path:?}"); + } + } + + #[test] + fn test_path_walker_v1() { + // 测试当前路径 + _test_path_walker_v1(current_dir().unwrap()); + } +} diff --git a/src/support/error_handling_boost.rs b/src/support/error_handling_boost.rs new file mode 100644 index 0000000..61004ee --- /dev/null +++ b/src/support/error_handling_boost.rs @@ -0,0 +1,36 @@ +//! 增强的快捷错误处理 +//! * 🎯用于(在命令行)快速处理、输出各种错误 + +use crate::support::io::output_print::OutputType; +use anyhow::{anyhow, Error}; +use std::fmt::Debug; + +/// 打印错误 +/// * 🚩在标准错误中打印基于[`Debug`]的信息 +/// * 🎯快速表示「报错而非panic」 +/// * 🚩【2024-04-02 18:59:19】不建议使用:不应向用户打印大量错误堆栈信息 +/// * ✨替代用法可参考[`crate::eprintln_cli`] +#[deprecated = "不建议使用:不应向用户打印大量错误堆栈信息"] +pub fn println_error(e: &impl Debug) { + // ! 无法在此直接使用:macro-expanded `macro_export` macros from the current crate cannot be referred to by absolute paths + // * 🚩【2024-04-02 16:33:47】目前处理办法:直接展开 + println!("{}", OutputType::Error.format_line(&format!("{e:?}"))); +} + +/// 打印错误 +/// * 🚩在标准错误中打印基于[`Debug`]的信息 +/// * 🎯快速表示「报错而非panic」 +/// * 🎯用于「传入所有权而非不可变引用」的[`Result::unwrap_or_else`] +/// * 🚩【2024-04-02 18:59:19】不建议使用:不应向用户打印大量错误堆栈信息 +/// * ✨替代用法可参考[`crate::eprintln_cli`] +#[deprecated = "不建议使用:不应向用户打印大量错误堆栈信息"] +pub fn println_error_owned(e: impl Debug) { + println!("{}", OutputType::Error.format_line(&format!("{e:?}"))); +} + +/// 将错误转换为[`anyhow::Error`] +/// * 🚩将错误转换为[`Debug`]信息,装入[`anyhow::Error`]中 +/// * 🎯在线程通信中安全抛出未实现[`Send`]的[`std::sync::PoisonError`] +pub fn error_anyhow(e: impl Debug) -> Error { + anyhow!("{e:?}") +} diff --git a/src/support/io/mod.rs b/src/support/io/mod.rs new file mode 100644 index 0000000..f53bcd4 --- /dev/null +++ b/src/support/io/mod.rs @@ -0,0 +1,17 @@ +//! 用于管理启动器的输入输出 +//! * ✨终端美化相关 +//! + +nar_dev_utils::mods! { + // 输出打印 + pub output_print; + + // 读取行迭代器 + pub readline_iter; + + // NAVM输出缓存 + pub navm_output_cache; + + // Websocket支持 + pub websocket; +} diff --git a/src/support/io/navm_output_cache.rs b/src/support/io/navm_output_cache.rs new file mode 100644 index 0000000..cd8301f --- /dev/null +++ b/src/support/io/navm_output_cache.rs @@ -0,0 +1,118 @@ +//! NAVM输出缓存 +//! * 🎯一站式存储、展示与管理NAVM的输出 +//! * 🎯可被其它二进制库所复用 + +use crate::support::error_handling_boost::error_anyhow; +use anyhow::Result; +use babel_nar::{ + output_handler::flow_handler_list::{FlowHandlerList, HandleResult}, + test_tools::VmOutputCache, +}; +use nar_dev_utils::ResultBoost; +use navm::output::Output; +use std::{ + ops::ControlFlow, + sync::{Arc, Mutex, MutexGuard}, +}; + +/// 线程间可变引用计数的别名 +pub type ArcMutex = Arc>; + +/// 输出缓存 +/// * 🎯统一「加入输出⇒打印输出」的逻辑 +/// * 🚩仅封装一个[`Vec`],而不对其附加任何[`Arc`]、[`Mutex`]的限定 +/// * ❌【2024-04-03 01:43:13】[`Arc`]必须留给[`RuntimeManager`]:需要对其中键的值进行引用 +#[derive(Debug)] +pub struct OutputCache { + /// 内部封装的输出数组 + /// * 🚩【2024-04-03 01:43:41】不附带任何包装类型,仅包装其自身 + pub(crate) inner: Vec, + + /// 流式侦听器列表 + /// * 🎯用于功能解耦、易分派的「NAVM输出处理」 + /// * 📌可在此过程中对输出进行拦截、转换等操作 + /// * 🎯CLI输出打印 + /// * 🎯Websocket输出回传(JSON) + pub output_handlers: FlowHandlerList, +} + +/// 功能实现 +impl OutputCache { + /// 构造函数 + pub fn new(inner: Vec) -> Self { + Self { + inner, + output_handlers: FlowHandlerList::new(), + } + } + + /// 不可变借用内部 + pub fn borrow_inner(&self) -> &Vec { + &self.inner + } + + /// 可变借用内部 + pub fn borrow_inner_mut(&mut self) -> &mut Vec { + &mut self.inner + } + + /// 默认[`Arc`]<[`Mutex`]> + pub fn default_arc_mutex() -> ArcMutex { + Arc::new(Mutex::new(Self::default())) + } + + /// 从[`Arc`]<[`Mutex`]>中解锁 + pub fn unlock_arc_mutex(arc_mutex: &mut ArcMutex) -> Result> { + arc_mutex.lock().transform_err(error_anyhow) + } + + /// 静默存入输出 + /// * 🎯内部可用的「静默存入输出」逻辑 + /// * 🚩【2024-04-03 01:07:55】不打算封装了 + pub fn put_silent(&mut self, output: Output) -> Result<()> { + // 加入输出 + self.inner.push(output); + Ok(()) + } +} + +/// 默认构造:空数组 +impl Default for OutputCache { + fn default() -> Self { + Self::new(vec![]) + } +} + +/// 实现「输出缓存」 +/// * 🚩【2024-04-03 14:33:50】不再涉及任何[`Arc`]或[`Mutex`] +impl VmOutputCache for OutputCache { + /// 存入输出 + /// * 🎯统一的「打印输出」逻辑 + /// * 🚩【2024-04-03 01:07:55】不打算封装了 + fn put(&mut self, output: Output) -> Result<()> { + // 交给处理者处理 + let r = self.output_handlers.handle(output); + match r { + // 通过⇒静默加入输出 + HandleResult::Passed(output) => self.put_silent(output), + // 被消耗⇒提示 + HandleResult::Consumed(index) => Ok(println!("NAVM输出在[{index}]位置被拦截。")), + } + } + + /// 遍历输出 + /// * 🚩不是返回迭代器,而是用闭包开始计算 + fn for_each(&self, mut f: impl FnMut(&Output) -> ControlFlow) -> Result> { + // 遍历 + for output in self.inner.iter() { + // 基于控制流的运行 + match f(output) { + ControlFlow::Break(value) => return Ok(Some(value)), + ControlFlow::Continue(()) => {} + } + } + + // 返回 + Ok(None) + } +} diff --git a/src/support/io/output_print.rs b/src/support/io/output_print.rs new file mode 100644 index 0000000..32bcbf0 --- /dev/null +++ b/src/support/io/output_print.rs @@ -0,0 +1,293 @@ +//! 输出打印 +//! * 🎯用于规范化、统一、美化CLI输出 +//! * 📌不仅仅是NAVM的输出 +//! +//! ## 输出美化参考 +//! +//! 输出美化逻辑参考了如下Julia代码: +//! +//! ```julia +//! """ +//! 用于高亮「输出颜色」的字典 +//! """ +//! const output_color_dict = Dict([ +//! NARSOutputType.IN => :light_white +//! NARSOutputType.OUT => :light_white +//! NARSOutputType.EXE => :light_cyan +//! NARSOutputType.ANTICIPATE => :light_yellow +//! NARSOutputType.ANSWER => :light_green +//! NARSOutputType.ACHIEVED => :light_green +//! NARSOutputType.INFO => :white +//! NARSOutputType.COMMENT => :white +//! NARSOutputType.ERROR => :light_red +//! NARSOutputType.OTHER => :light_black # * 未识别的信息 +//! # ! ↓这俩是OpenNARS附加的 +//! "CONFIRM" => :light_blue +//! "DISAPPOINT" => :light_magenta +//! ]) +//! +//! """ +//! 用于分派「颜色反转」的集合 +//! """ +//! const output_reverse_color_dict = Set([ +//! NARSOutputType.EXE +//! NARSOutputType.ANSWER +//! NARSOutputType.ACHIEVED +//! ]) +//! ``` +//! +//! * 最后更新:【2024-04-02 15:54:23】 +//! * 参考链接: + +use colored::Colorize; +use nar_dev_utils::manipulate; +use narsese::conversion::string::impl_lexical::format_instances::FORMAT_ASCII; +use navm::output::Output; +use std::fmt::Display; + +/// 统一的「CLI输出类型」 +#[derive(Debug, Clone, Copy)] +pub enum OutputType<'a> { + /// NAVM输出 + /// * 🚩【2024-04-02 15:42:44】目前因NAVM的[`Output`]仅有`enum`结构而无「类型」标签, + /// * 无法复用NAVM的枚举 + Vm(&'a str), + /// CLI错误 + Error, + /// CLI警告 + Warn, + /// CLI信息 + Info, + /// CLI日志 + Log, + /// CLI debug + Debug, +} + +impl OutputType<'_> { + /// 自身的字符串形式 + /// * 🎯作为输出的「头部」 + pub fn as_str(&self) -> &str { + match self { + OutputType::Vm(s) => s, + OutputType::Error => "ERROR", + OutputType::Warn => "WARN", + OutputType::Info => "INFO", + OutputType::Debug => "DEBUG", + OutputType::Log => "LOG", + } + } + + /// 格式化CLI输出 + /// * 🎯封装标准输出形式:`[类型] 内容` + /// * 🎯封装命令行美化逻辑 + #[inline(always)] + pub fn format_line(&self, msg: &str) -> impl Display { + self.to_colored_str(format!("[{}] {}", self.as_str(), msg)) + } + + /// 从NAVM输出格式化 + /// * 🎯封装「从NAVM输出打印」 + #[inline(always)] + pub fn format_navm_output(out: &Output) -> impl Display { + let message = manipulate!( + // 新建字符串对象 + String::new() + // 格式化头部 + => Self::format_navm_output_type(out, _) + // 格式化原始内容 + => Self::format_navm_output_content(out, _) + ); + // 载入着色 + OutputType::from(out).to_colored_str(message) + } + + /// 从NAVM输出格式化(详细) + /// * 🎯封装「从NAVM输出打印」 + /// * ✨提供「解析出的Narsese」与「解析出的NARS操作」信息 + #[inline(always)] + pub fn format_from_navm_output_verbose(out: &Output) -> impl Display { + let message = manipulate!( + // 新建字符串对象 + String::new() + // 格式化头部 + => Self::format_navm_output_type(out, _) + // 详细格式化:Narsese、NARS操作 + => Self::format_navm_output_verbose(out, _) + // 格式化原始内容 + => Self::format_navm_output_content(out, _) + ); + // 载入 + OutputType::from(out).to_colored_str(message) + } + + /// 从NAVM输出格式化(详细) + /// * 🎯封装「从NAVM输出打印」逻辑 + /// * 🚩基于「流式添加内容」的做法 + /// * 📄`[OUT]` + #[inline(always)] + fn format_navm_output_type(out: &Output, out_message: &mut String) { + // 返回创建的字符串 + *out_message += "["; + *out_message += out.type_name(); + *out_message += "] "; // ! 🚩使用尾缀空格,以避免「非必要连续空格」 + } + + /// 从NAVM输出格式化(详细) + /// * 🎯封装「从NAVM输出打印」逻辑 + /// * 🚩基于「流式添加内容」的做法 + /// * 📄`[# B>. #]` + #[inline(always)] + fn format_navm_output_verbose(out: &Output, out_message: &mut String) { + // * 🚩先添加Narsese + if let Some(narsese) = out.get_narsese() { + *out_message += "[# "; + *out_message += &(FORMAT_ASCII.format(narsese)); + *out_message += " #]"; + *out_message += " "; // 🚩使用尾缀空格,以避免「非必要连续空格」 + } + // * 🚩再添加操作 + if let Some(operation) = out.get_operation() { + *out_message += "[% "; + // 🚩↓使用尾缀空格,以避免「非必要连续空格」 + *out_message += &operation.to_string(); + *out_message += " %]"; + *out_message += " "; // 🚩使用尾缀空格,以避免「非必要连续空格」 + } + } + + /// * 📄ONA:`Input: G3! :|: occurrenceTime=37 Priority=1.000000 Truth: frequency=1.000000, confidence=0.900000` + fn format_navm_output_content(out: &Output, out_message: &mut String) { + // 最后添加原始内容 + *out_message += out.get_content().trim(); + } + + /// 基于[`colored`]的输出美化 + /// * 🎯用于CLI的彩色输出 + /// * 🔗参考Julia版本 + pub fn to_colored_str(&self, message: String) -> impl Display { + match self.as_str() { + // CLI独有 + "DEBUG" => message.bright_blue(), + "WARN" => message.bright_yellow(), + "LOG" => message.bright_black(), + // NAVM输出 + "IN" | "OUT" => message.bright_white(), + "EXE" => message.bright_cyan().reversed(), + "ANSWER" | "ACHIEVED" => message.bright_green().reversed(), + "INFO" => message.cyan(), + "COMMENT" => message.white(), + "ERROR" => message.red(), + "TERMINATED" => message.bright_white().reversed().blink(), + // ↓OpenNARS附加 + "ANTICIPATE" => message.bright_yellow(), + "CONFIRM" => message.bright_blue(), + "DISAPPOINT" => message.bright_magenta(), + // 默认 / 其它 + "OTHER" => message.bright_black(), + _ => message.bright_white(), + } + // 参考Julia,始终加粗 + .bold() + } + + /// ✨格式化打印CLI输出 + /// * 🎯BabelNAR CLI + #[inline] + pub fn print_line(&self, message: &str) { + println!("{}", self.format_line(message)); + } + + /// ✨格式化打印NAVM输出 + /// * 🎯BabelNAR CLI + #[inline] + pub fn print_navm_output(out: &Output) { + println!("{}", Self::format_navm_output(out)); + } + + /// ✨格式化打印NAVM输出(详细) + /// * 🎯BabelNAR CLI + /// * 🎯附带debug效果(检验「输出转译是否成功达到预期」) + #[inline] + pub fn print_navm_output_verbose(out: &Output) { + println!("{}", Self::format_from_navm_output_verbose(out)); + } + + /// ✨格式化打印CLI输出(标准错误) + /// * 🎯BabelNAR CLI + #[inline] + pub fn eprint_line(&self, message: &str) { + eprintln!("{}", self.format_line(message)); + } + + /// ✨格式化打印NAVM输出(标准错误) + /// * 🎯BabelNAR CLI + #[inline] + pub fn eprint_navm_output(out: &Output) { + eprintln!("{}", Self::format_navm_output(out)); + } + + /// ✨格式化打印NAVM输出(标准错误)(详细) + /// * 🎯BabelNAR CLI + /// * 🎯附带debug效果(检验「输出转译是否成功达到预期」) + #[inline] + pub fn eprint_navm_output_verbose(out: &Output) { + eprintln!("{}", Self::format_from_navm_output_verbose(out)); + } +} + +/// 快捷打印宏 +#[macro_export] +macro_rules! println_cli { + // 消息 | ✨可格式化 + ([$enum_type_name:ident] $($tail:tt)*) => { + // 调用内部函数 + $crate::cli_support::io::output_print::OutputType::$enum_type_name.print_line(&format!($($tail)*)); + }; + // NAVM输出 表达式 + ($navm_output:expr) => { + // 调用内部函数 + $crate::cli_support::io::output_print::OutputType::print_navm_output($navm_output); + }; + // NAVM输出 表达式 | 🪄详细 + (% $navm_output:expr) => { + // 调用内部函数 + $crate::cli_support::io::output_print::OutputType::print_navm_output_verbose($navm_output); + }; +} + +/// 快捷打印宏/标准错误 +#[macro_export] +macro_rules! eprintln_cli { + // 消息 | ✨可格式化 + ([$enum_type_name:ident] $($tail:tt)*) => { + // 调用内部函数 + $crate::cli_support::io::output_print::OutputType::$enum_type_name.eprint_line(&format!($($tail)*)); + }; + // NAVM输出 表达式 + ($navm_output:expr) => { + // 调用内部函数 + $crate::cli_support::io::output_print::OutputType::eprint_navm_output($navm_output); + }; + // NAVM输出 表达式 | 🪄详细 + (% $navm_output:expr) => { + // 调用内部函数 + $crate::cli_support::io::output_print::OutputType::eprint_navm_output_verbose($navm_output); + }; +} + +/// 快捷打印宏/当输出为`Err`时打印,当Ok时为值 +#[macro_export] +macro_rules! if_let_err_eprintln_cli { + { $value:expr => $e:ident => $($tail:tt)* } => { + if let Err($e) = $value { + eprintln_cli!($($tail)*); + } + }; +} + +impl<'a> From<&'a Output> for OutputType<'a> { + fn from(out: &'a Output) -> Self { + OutputType::Vm(out.type_name()) + } +} diff --git a/src/support/io/readline_iter.rs b/src/support/io/readline_iter.rs new file mode 100644 index 0000000..f9874b1 --- /dev/null +++ b/src/support/io/readline_iter.rs @@ -0,0 +1,61 @@ +//! 读取行迭代器 +//! * 🎯以迭代器的语法获取、处理用户输入 +//! * ❌【2024-04-03 14:28:02】放弃「泛型化改造」:[`Stdin`]能`read_line`,但却没实现[`std::io::BufRead`] + +use crate::support::io::output_print::OutputType; +use std::io::{stdin, stdout, Result as IoResult, Stdin, Write}; + +/// 读取行迭代器 +/// * 🚩每迭代一次,请求用户输入一行 +/// * ✨自动清空缓冲区 +/// * ❌无法在【不复制字符串】的情况下实现「迭代出所输入内容」的功能 +/// * ❌【2024-04-02 03:49:56】无论如何都无法实现:迭代器物件中引入就必须碰生命周期 +/// * 🚩最终仍需复制字符串:调用处方便使用 +/// * ❓是否需要支持提示词 +#[derive(Debug)] +pub struct ReadlineIter { + /// 内置的「输入内容缓冲区」 + buffer: String, + /// 内置的「标准输入」 + stdin: Stdin, + /// 输入提示词 + prompt: String, +} + +impl ReadlineIter { + pub fn new(prompt: impl Into) -> Self { + Self { + buffer: String::new(), + stdin: stdin(), + prompt: prompt.into(), + } + } +} + +impl Default for ReadlineIter { + fn default() -> Self { + Self::new("") + } +} + +/// 实现迭代器 +impl Iterator for ReadlineIter { + type Item = IoResult; + + fn next(&mut self) -> Option { + // 清空缓冲区 + self.buffer.clear(); + // 打印提示词 + print!("{}", self.prompt); + if let Err(e) = stdout().flush() { + OutputType::Warn.print_line(&format!("无法冲洗输出: {e}")); + } + // 读取一行 + // * 📝`stdin()`是懒加载的,只会获取一次,随后返回的都是引用对象 + if let Err(e) = self.stdin.read_line(&mut self.buffer) { + return Some(Err(e)); + } + // 返回 + Some(IoResult::Ok(self.buffer.clone())) + } +} diff --git a/src/support/io/websocket.rs b/src/support/io/websocket.rs new file mode 100644 index 0000000..ed4c4d3 --- /dev/null +++ b/src/support/io/websocket.rs @@ -0,0 +1,217 @@ +//! 基于[`ws`]为CLI提供Websocket IO支持 +//! * ✨简单的「地址生成」「服务端启动」等逻辑 +//! * ⚠️不涉及具体业务代码 +//! * 📝【2024-04-03 16:01:37】可以启动IPv6服务端,但尚且没有测试方法 +//! * 📌语法:`[主机地址]:连接端口` +//! * 📄示例:`[::]:3012` +//! * 🔗参考: + +use anyhow::Result; +use std::{ + fmt, + net::ToSocketAddrs, + thread::{self, JoinHandle}, +}; +use ws::{Factory, Handler, Sender, WebSocket}; + +/// 从「主机地址」与「连接端口」格式化到「完整地址」 +/// * ✨兼容IPv4 和 IPv6 +/// * 📌端口号为十六位无符号整数(0~65535,含两端) +pub fn to_address(host: &str, port: u16) -> String { + match is_ipv6_host(host) { + // IPv6 + true => format!("[{host}]:{port}"), + // IPv4 + false => format!("{host}:{port}"), + } +} + +/// 判断**主机地址**是否为IPv6地址 +/// * 🚩【2024-04-03 17:20:59】目前判断标准:地址中是否包含冒号 +/// * 📄`::1` +/// * 📄`fe80::abcd:fade:dad1` +pub fn is_ipv6_host(host: &str) -> bool { + host.contains(':') +} + +/// 生成一个Websocket监听线程 +/// * 🎯简单生成一个Websocket监听线程 +/// * ⚠️线程在生成后立即开始运行 +#[inline] +pub fn spawn_on(addr: A, message_listener: F) -> JoinHandle> +where + A: ToSocketAddrs + fmt::Debug + Send + Sync + 'static, + F: FnMut(Sender) -> H + Send + Sync + 'static, + H: Handler, +{ + spawn_server(addr, message_listener) +} + +/// 生成一个Websocket监听线程,使用特定的服务端 +/// * 🎯使用自定义的Websocket「工厂」[`Factory`]生成服务端与连接处理者(侦听器) +/// * 📄地址格式:`127.0.0.1:8080`、`localhost:8080` +/// * ⚠️线程在生成后立即开始运行 +/// * 📝与[`ws::listen`]不同的是:允许在[`factory`]处自定义各种连接、侦听逻辑 +/// * 🔗参考: +#[inline] +pub fn spawn_server(address: A, factory: F) -> JoinHandle> +where + A: ToSocketAddrs + fmt::Debug + Send + Sync + 'static, + F: Factory + Send + Sync + 'static, + H: Handler, +{ + thread::spawn(move || { + let server = WebSocket::new(factory)?; + server.listen(address)?; + Ok(()) + }) +} + +/// 单元测试 +#[cfg(test)] +mod tests { + use super::*; + use ws::util::{Timeout, Token}; + + #[test] + fn main() { + let t = spawn_on("127.0.0.1:3012", |sender| { + println!("Websocket启动成功"); + move |msg| { + println!("Received: {}", msg); + sender.send(msg) + } + }); + t.join().expect("Websocket失败!").expect("Websocket出错!"); + } + + /// 📄简单的回传连接处理者 + /// * 🎯处理单个Websocket连接 + #[derive(Debug)] + struct EchoHandler { + sender: Sender, + } + + impl Handler for EchoHandler { + fn on_shutdown(&mut self) { + println!("Handler received WebSocket shutdown request."); + } + + fn on_open(&mut self, shake: ws::Handshake) -> ws::Result<()> { + if let Some(addr) = shake.remote_addr()? { + println!("Connection with {} now open", addr); + } + Ok(()) + } + + fn on_message(&mut self, msg: ws::Message) -> ws::Result<()> { + println!("Received message {:?}", msg); + self.sender.send(msg)?; + Ok(()) + } + + fn on_close(&mut self, code: ws::CloseCode, reason: &str) { + println!("Connection closing due to ({:?}) {}", code, reason); + } + + fn on_error(&mut self, err: ws::Error) { + // Ignore connection reset errors by default, but allow library clients to see them by + // overriding this method if they want + if let ws::ErrorKind::Io(ref err) = err.kind { + if let Some(104) = err.raw_os_error() { + return; + } + } + + eprintln!("{:?}", err); + } + + fn on_request(&mut self, req: &ws::Request) -> ws::Result { + println!("Handler received request:\n{}", req); + ws::Response::from_request(req) + } + + fn on_response(&mut self, res: &ws::Response) -> ws::Result<()> { + println!("Handler received response:\n{}", res); + Ok(()) + } + + fn on_timeout(&mut self, event: Token) -> ws::Result<()> { + println!("Handler received timeout token: {:?}", event); + Ok(()) + } + + fn on_new_timeout(&mut self, _: Token, _: Timeout) -> ws::Result<()> { + // default implementation discards the timeout handle + Ok(()) + } + + fn on_frame(&mut self, frame: ws::Frame) -> ws::Result> { + println!("Handler received: {}", frame); + // default implementation doesn't allow for reserved bits to be set + if frame.has_rsv1() || frame.has_rsv2() || frame.has_rsv3() { + Err(ws::Error::new( + ws::ErrorKind::Protocol, + "Encountered frame with reserved bits set.", + )) + } else { + Ok(Some(frame)) + } + } + + fn on_send_frame(&mut self, frame: ws::Frame) -> ws::Result> { + println!("Handler will send: {}", frame); + // default implementation doesn't allow for reserved bits to be set + if frame.has_rsv1() || frame.has_rsv2() || frame.has_rsv3() { + Err(ws::Error::new( + ws::ErrorKind::Protocol, + "Encountered frame with reserved bits set.", + )) + } else { + Ok(Some(frame)) + } + } + } + + struct EchoFactory; + + impl Factory for EchoFactory { + type Handler = EchoHandler; + + fn connection_made(&mut self, sender: Sender) -> EchoHandler { + dbg!(EchoHandler { sender }) + } + + fn on_shutdown(&mut self) { + println!("Factory received WebSocket shutdown request."); + } + + fn client_connected(&mut self, ws: Sender) -> Self::Handler { + self.connection_made(ws) + } + + fn server_connected(&mut self, ws: Sender) -> Self::Handler { + self.connection_made(ws) + } + + fn connection_lost(&mut self, handler: Self::Handler) { + println!("Connection lost of {handler:?}"); + } + } + + #[test] + #[ignore = "【2024-09-12 18:02:52】涉及网络IO的不进行测试,会导致测试进程阻塞"] + fn test_echo_localhost() -> Result<()> { + let ws = WebSocket::new(EchoFactory)?; + ws.listen("localhost:3012")?; + Ok(()) + } + + #[test] + #[ignore = "【2024-09-12 18:02:52】涉及网络IO的不进行测试,会导致测试进程阻塞"] + fn test_echo_ipv6() -> Result<()> { + let ws = WebSocket::new(EchoFactory)?; + ws.listen("[::]:3012")?; + Ok(()) + } +} diff --git a/src/support/mod.rs b/src/support/mod.rs new file mode 100644 index 0000000..66cb24a --- /dev/null +++ b/src/support/mod.rs @@ -0,0 +1,17 @@ +//! 命令行支持 +//! * 🕒原BabelNAR.rs `src/cli_support/*.rs` +//! * 🚩【2024-09-12 17:41:35】现在统一放置在`src/cli`下 +//! * 🎯避免「CLI修改功能需要动上级模块代码」的情况 +//! * 🎯通用、可选地复用「CIN启动器」等「命令行工具」的内容 +//! * 🎯亦可为后续基于UI的应用提供支持 + +nar_dev_utils::mods! { + // CIN搜索 + pub cin_search; + + // 输入输出 + pub io; +} + +// 错误处理增强 +pub mod error_handling_boost;