feat: 拆分 mirror-core
- 拆分 mirror-core,优化项目结构
This commit is contained in:
@@ -0,0 +1,89 @@
|
||||
use crate::{Error, Result};
|
||||
use mlua::LuaString;
|
||||
use std::ffi::{OsStr, OsString};
|
||||
use std::fmt;
|
||||
|
||||
/// Lua 字符串 -> OsString(Linux 直接透传字节,Windows 回退到系统代码页)
|
||||
pub(crate) fn lua_string_2_os_string(s: &LuaString) -> Result<OsString> {
|
||||
let raw_bytes = &s.as_bytes();
|
||||
// 1. Unix 平台:直接零拷贝透传原始字节(无损支持任意编码)
|
||||
#[cfg(unix)]
|
||||
{
|
||||
use std::os::unix::ffi::OsStrExt;
|
||||
Ok(OsStr::from_bytes(raw_bytes).to_os_string())
|
||||
}
|
||||
// 2. Windows 平台:优先按 UTF-8 解码,失败则按当前系统本地代码页 (ANSI/GBK) 转换
|
||||
#[cfg(windows)]
|
||||
{
|
||||
// 先尝试标准的 UTF-8
|
||||
if let Ok(utf8_str) = std::str::from_utf8(raw_bytes) {
|
||||
return Ok(OsString::from(utf8_str));
|
||||
}
|
||||
|
||||
unsafe {
|
||||
use std::os::windows::ffi::OsStringExt;
|
||||
use windows_sys::Win32::Globalization::{
|
||||
CP_ACP, MB_ERR_INVALID_CHARS, MultiByteToWideChar,
|
||||
};
|
||||
|
||||
if raw_bytes.is_empty() {
|
||||
return Ok(OsString::new());
|
||||
}
|
||||
|
||||
let len = MultiByteToWideChar(
|
||||
CP_ACP,
|
||||
MB_ERR_INVALID_CHARS,
|
||||
raw_bytes.as_ptr(),
|
||||
raw_bytes.len() as i32,
|
||||
std::ptr::null_mut(),
|
||||
0,
|
||||
);
|
||||
|
||||
if len <= 0 {
|
||||
// 构造一个带上下文的 FromLua 转换错误,便于定位配置问题
|
||||
return Err(Error::EncodingError {
|
||||
from: "LuaString",
|
||||
to: "OsString".to_string(),
|
||||
message: format!("字符串{:?}包含无效或当前系统无法识别的编码字节", s),
|
||||
});
|
||||
}
|
||||
|
||||
let mut buf = vec![0u16; len as usize];
|
||||
MultiByteToWideChar(
|
||||
CP_ACP,
|
||||
MB_ERR_INVALID_CHARS,
|
||||
raw_bytes.as_ptr(),
|
||||
raw_bytes.len() as i32,
|
||||
buf.as_mut_ptr(),
|
||||
len,
|
||||
);
|
||||
|
||||
Ok(OsString::from_wide(&buf))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 跨平台检查 `OsStr` 中是否含 NUL 字符
|
||||
pub(crate) fn ensure_no_nul(context: &impl fmt::Display, os_str: &OsStr) -> Result<()> {
|
||||
#[cfg(unix)]
|
||||
{
|
||||
use std::os::unix::ffi::OsStrExt;
|
||||
if os_str.as_bytes().contains(&0) {
|
||||
return Err(Error::ContainsNul(format!(
|
||||
"{} 的值不能包含 NUL 字符",
|
||||
context
|
||||
)));
|
||||
}
|
||||
}
|
||||
#[cfg(windows)]
|
||||
{
|
||||
use std::os::windows::ffi::OsStrExt;
|
||||
if os_str.encode_wide().any(|c| c == 0) {
|
||||
return Err(Error::ContainsNul(format!(
|
||||
"{} 的值不能包含 NUL 字符",
|
||||
context
|
||||
)));
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
@@ -0,0 +1,132 @@
|
||||
//! Lua 边界层:只负责把 Lua 值搬成 DTO,不做任何语义判断。
|
||||
//!
|
||||
//! 这一层实现的是 mlua 的 `FromLua`,错误天然是 `mlua::Error`。
|
||||
//! 领域规则(必填字段、类型、编码、命令名、别名环)全部在 `model.rs` 的
|
||||
//! `TryFrom` 里,那里的错误统一是 `crate::Error`。
|
||||
|
||||
use super::model::{MirrorSpec, TargetSpec};
|
||||
use mlua::{FromLua, Lua, Table, Value};
|
||||
|
||||
/// 注册表 DTO:键值对原样保留,命令名合法性交给领域层判断。
|
||||
pub(crate) struct MirrorSpecDto {
|
||||
pub entries: Vec<(Value, TargetSpecDto)>,
|
||||
}
|
||||
|
||||
impl FromLua for MirrorSpecDto {
|
||||
fn from_lua(value: Value, _lua: &Lua) -> mlua::Result<Self> {
|
||||
let root = value
|
||||
.as_table()
|
||||
.ok_or_else(|| mlua::Error::FromLuaConversionError {
|
||||
from: value.type_name(),
|
||||
to: "Table".into(),
|
||||
message: Some("mirror 全局注册表顶层必须是 table".into()),
|
||||
})?;
|
||||
|
||||
let mut entries = Vec::new();
|
||||
for pair in root.pairs::<Value, Value>() {
|
||||
let (key, entry) = pair?;
|
||||
let entry = entry
|
||||
.as_table()
|
||||
.ok_or_else(|| mlua::Error::FromLuaConversionError {
|
||||
from: entry.type_name(),
|
||||
to: "Table".into(),
|
||||
message: Some(format!(
|
||||
"目标工具 '{}' 的配置必须是 table,实际是 {}",
|
||||
display_key(&key),
|
||||
entry.type_name()
|
||||
)),
|
||||
})?;
|
||||
entries.push((key, TargetSpecDto::from_table(&entry)?));
|
||||
}
|
||||
Ok(Self { entries })
|
||||
}
|
||||
}
|
||||
|
||||
/// 单个目标工具配置的 DTO:字段原样保留,类型判断留给领域层。
|
||||
///
|
||||
/// 字段一律用 `Value`:Lua 里"字段不存在"和"字段是 nil"是同一件事,
|
||||
/// 都用 `Value::Nil` 表示。用 `Option<Value>` 反而会有两种"缺失"写法。
|
||||
pub(crate) struct TargetSpecDto {
|
||||
pub target: TargetDto,
|
||||
pub args: Value,
|
||||
pub env: Value,
|
||||
pub aliases: Value,
|
||||
}
|
||||
|
||||
impl TargetSpecDto {
|
||||
fn from_table(table: &Table) -> mlua::Result<Self> {
|
||||
Ok(Self {
|
||||
target: TargetDto::from_value(table.get::<Value>("target")?)?,
|
||||
args: table.get::<Value>("args")?,
|
||||
env: table.get::<Value>("env")?,
|
||||
aliases: table.get::<Value>("aliases")?,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/// target 的"形状":标量还是多版本 table。
|
||||
///
|
||||
/// `Value::Nil`(字段没写)会落进 `Scalar`,由领域层决定它是错误还是默认值。
|
||||
/// 字符串能不能当路径、table 里 `home` 是否必填,也都留给领域层判断。
|
||||
pub(crate) enum TargetDto {
|
||||
Scalar(Value),
|
||||
Versioned {
|
||||
home: Value,
|
||||
version_file: Value,
|
||||
current: Value,
|
||||
},
|
||||
}
|
||||
|
||||
impl TargetDto {
|
||||
fn from_value(value: Value) -> mlua::Result<Self> {
|
||||
match value {
|
||||
Value::Table(table) => Ok(Self::Versioned {
|
||||
home: table.get::<Value>("home")?,
|
||||
version_file: table.get::<Value>("version_file")?,
|
||||
current: table.get::<Value>("current")?,
|
||||
}),
|
||||
other => Ok(Self::Scalar(other)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 错误信息里显示注册表 key;非字符串 key 退化成类型名。
|
||||
fn display_key(key: &Value) -> String {
|
||||
match key {
|
||||
Value::String(s) => s
|
||||
.to_str()
|
||||
.map(|s| s.to_string())
|
||||
.unwrap_or_else(|_| "<非 UTF-8>".to_string()),
|
||||
other => other.type_name().to_string(),
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// 兼容适配器
|
||||
//
|
||||
// 消费方目前仍使用 `runtime.eval_script::<MirrorRegistry>(...)` 和
|
||||
// `MirrorTarget::from_lua(...)`,所以保留这两个薄适配器:
|
||||
// 边界提取(DTO)→ 领域转换(TryFrom)→ 错误翻译(Into)。
|
||||
// 领域逻辑一行都不在这两个 impl 里。
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
impl FromLua for MirrorSpec {
|
||||
fn from_lua(value: Value, lua: &Lua) -> mlua::Result<Self> {
|
||||
let dto = MirrorSpecDto::from_lua(value, lua)?;
|
||||
MirrorSpec::try_from(dto).map_err(Into::into)
|
||||
}
|
||||
}
|
||||
|
||||
impl FromLua for TargetSpec {
|
||||
fn from_lua(value: Value, _lua: &Lua) -> mlua::Result<Self> {
|
||||
let table = value
|
||||
.as_table()
|
||||
.ok_or_else(|| mlua::Error::FromLuaConversionError {
|
||||
from: value.type_name(),
|
||||
to: "Table".into(),
|
||||
message: Some("目标工具配置必须是 table".into()),
|
||||
})?;
|
||||
let dto = TargetSpecDto::from_table(&table)?;
|
||||
TargetSpec::try_from(dto).map_err(Into::into)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
pub(crate) mod convert;
|
||||
pub(crate) mod dto;
|
||||
pub(crate) mod model;
|
||||
pub(crate) mod naming;
|
||||
pub(crate) mod values;
|
||||
|
||||
pub use model::{MirrorSpec, Target, TargetSpec};
|
||||
pub use naming::parse_command_name;
|
||||
|
||||
// 兼容旧名:mirror-shim / mirror-cli 目前仍按这些名字引用。
|
||||
// 等消费方迁移到 MirrorSpec / TargetSpec / Target 后可以删掉。
|
||||
pub type MirrorRegistry = MirrorSpec;
|
||||
pub type MirrorTarget = TargetSpec;
|
||||
pub type TargetConfig = Target;
|
||||
|
||||
use crate::{LuaRuntime, Result};
|
||||
use std::path::Path;
|
||||
|
||||
/// 配置加载入口:边界提取(DTO)→ 领域转换(TryFrom)。
|
||||
///
|
||||
/// 调用方拿到的错误全部是 `crate::Error`;DTO 与 mlua 的错误类型
|
||||
/// 都被关在这个函数内部。
|
||||
pub fn load_mirror_spec(runtime: &LuaRuntime, path: impl AsRef<Path>) -> Result<MirrorSpec> {
|
||||
let dto: dto::MirrorSpecDto = runtime.eval_script(path)?;
|
||||
MirrorSpec::try_from(dto)
|
||||
}
|
||||
@@ -0,0 +1,268 @@
|
||||
//! 领域模型:只描述"配置应该长什么样",不实现 mlua 的 `FromLua`。
|
||||
//!
|
||||
//! 两阶段加载:
|
||||
//! 1. `config::dto` 把 Lua 值搬成 DTO(边界,错误是 mlua 形状)
|
||||
//! 2. 本文件的 `TryFrom<DTO>` 做语义校验与类型转换(错误全部是 `crate::Error`)
|
||||
|
||||
use crate::config::convert::{ensure_no_nul, lua_string_2_os_string};
|
||||
use crate::config::dto::{MirrorSpecDto, TargetDto, TargetSpecDto};
|
||||
use crate::config::values::{parse_aliases, parse_args, parse_env};
|
||||
use crate::{Error, Result, parse_command_name};
|
||||
use mlua::Value;
|
||||
use std::collections::HashMap;
|
||||
use std::ffi::{OsStr, OsString};
|
||||
use std::path::PathBuf;
|
||||
|
||||
pub struct MirrorSpec {
|
||||
pub targets: HashMap<String, TargetSpec>,
|
||||
}
|
||||
|
||||
impl TryFrom<MirrorSpecDto> for MirrorSpec {
|
||||
type Error = Error;
|
||||
|
||||
fn try_from(dto: MirrorSpecDto) -> Result<Self> {
|
||||
let mut targets = HashMap::with_capacity(dto.entries.len());
|
||||
for (key, tool) in dto.entries {
|
||||
let name = parse_command_name(&key)?;
|
||||
targets.insert(name, TargetSpec::try_from(tool)?);
|
||||
}
|
||||
Ok(Self { targets })
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Default)]
|
||||
pub struct TargetSpec {
|
||||
/// 目标程序可执行文件路径
|
||||
pub target: Target,
|
||||
/// 默认预设参数列表
|
||||
pub args: Vec<OsString>,
|
||||
/// 参数别名映射表(用于展开以 mr: 为前缀的别名)
|
||||
pub aliases: HashMap<String, Vec<OsString>>,
|
||||
/// 注入/覆盖的环境变量
|
||||
pub env: HashMap<String, OsString>,
|
||||
}
|
||||
|
||||
impl TargetSpec {
|
||||
pub fn resolve_args<I, S>(&self, args: I) -> Vec<OsString>
|
||||
where
|
||||
I: IntoIterator<Item = S>,
|
||||
S: AsRef<OsStr>,
|
||||
{
|
||||
crate::args::alias::expand_aliases(&self.aliases, args)
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<TargetSpecDto> for TargetSpec {
|
||||
type Error = Error;
|
||||
|
||||
fn try_from(dto: TargetSpecDto) -> Result<Self> {
|
||||
let target = Target::try_from(dto.target)?;
|
||||
|
||||
let args = match dto.args {
|
||||
Value::Nil => Vec::new(),
|
||||
value => parse_args(&value)?,
|
||||
};
|
||||
let aliases = match dto.aliases {
|
||||
Value::Nil => HashMap::new(),
|
||||
value => parse_aliases(&value)?,
|
||||
};
|
||||
let env = match dto.env {
|
||||
Value::Nil => HashMap::new(),
|
||||
value => parse_env(&value)?,
|
||||
};
|
||||
|
||||
Ok(Self {
|
||||
target,
|
||||
args,
|
||||
aliases,
|
||||
env,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/// 目标可执行文件的定位配置(支持静态路径与多版本 Table)
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum Target {
|
||||
/// 静态单版本工具
|
||||
Static(PathBuf),
|
||||
/// 多版本管理
|
||||
Versioned {
|
||||
/// 工具主根目录(供 mr use 扫描候选版本)
|
||||
home: PathBuf,
|
||||
/// 版本真相源文件名(如 ".version",可选,默认 ".version")
|
||||
version_file: Option<String>,
|
||||
/// 当前激活版本的可执行文件完整路径(若未激活或不存在则为 None)
|
||||
current: Option<PathBuf>,
|
||||
},
|
||||
}
|
||||
|
||||
impl Default for Target {
|
||||
fn default() -> Self {
|
||||
Target::Static(PathBuf::default())
|
||||
}
|
||||
}
|
||||
|
||||
impl Target {
|
||||
/// 垫片程序(mirror-shim)专用快速通道:提取最终可执行路径
|
||||
///
|
||||
/// - Static: 返回自身路径
|
||||
/// - Versioned: 返回 current 路径(若未激活则返回 None)
|
||||
pub fn executable_path(&self) -> Option<&PathBuf> {
|
||||
match self {
|
||||
Target::Static(path) => Some(path),
|
||||
Target::Versioned { current, .. } => current.as_ref(),
|
||||
}
|
||||
}
|
||||
|
||||
/// 管理程序(mr-cli)专用辅助方法:判断是否支持多版本
|
||||
pub fn is_versioned(&self) -> bool {
|
||||
matches!(self, Target::Versioned { .. })
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<TargetDto> for Target {
|
||||
type Error = Error;
|
||||
|
||||
fn try_from(dto: TargetDto) -> Result<Self> {
|
||||
match dto {
|
||||
TargetDto::Scalar(Value::String(s)) => {
|
||||
let os_str = lua_string_2_os_string(&s)?;
|
||||
ensure_no_nul(&"target", &os_str)?;
|
||||
Ok(Target::Static(PathBuf::from(os_str)))
|
||||
}
|
||||
TargetDto::Scalar(Value::Nil) => Err(Error::MissingField {
|
||||
field: "target(应为字符串路径或多版本配置表)".into(),
|
||||
}),
|
||||
TargetDto::Scalar(other) => Err(Error::TypeMismatch {
|
||||
field: "target".into(),
|
||||
expected: "有效路径字符串或多版本配置表",
|
||||
actual: other.type_name(),
|
||||
}),
|
||||
TargetDto::Versioned {
|
||||
home,
|
||||
version_file,
|
||||
current,
|
||||
} => {
|
||||
let home = required_path(home, "home(多版本 target 表)", "target.home")?;
|
||||
let version_file = optional_string(version_file, "target.version_file")?;
|
||||
let current = optional_path(current, "target.current")?;
|
||||
|
||||
Ok(Target::Versioned {
|
||||
home,
|
||||
version_file,
|
||||
current,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 必填路径字段:缺失/nil 报 `MissingField`,类型不符报 `TypeMismatch`
|
||||
fn required_path(value: Value, field: &str, context: &str) -> Result<PathBuf> {
|
||||
match value {
|
||||
Value::Nil => Err(Error::MissingField {
|
||||
field: field.to_string(),
|
||||
}),
|
||||
Value::String(s) => {
|
||||
let os_str = lua_string_2_os_string(&s)?;
|
||||
ensure_no_nul(&context, &os_str)?;
|
||||
Ok(PathBuf::from(os_str))
|
||||
}
|
||||
other => Err(Error::TypeMismatch {
|
||||
field: context.to_string(),
|
||||
expected: "路径字符串",
|
||||
actual: other.type_name(),
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
/// 可选字符串字段:缺失/nil 视为 None
|
||||
fn optional_string(value: Value, context: &str) -> Result<Option<String>> {
|
||||
match value {
|
||||
Value::Nil => Ok(None),
|
||||
Value::String(s) => {
|
||||
let os_str = lua_string_2_os_string(&s)?;
|
||||
ensure_no_nul(&context, &os_str)?;
|
||||
Ok(Some(os_str.to_string_lossy().into_owned()))
|
||||
}
|
||||
other => Err(Error::TypeMismatch {
|
||||
field: context.to_string(),
|
||||
expected: "字符串 (String)",
|
||||
actual: other.type_name(),
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
/// 可选路径字段:缺失/nil 视为 None
|
||||
fn optional_path(value: Value, context: &str) -> Result<Option<PathBuf>> {
|
||||
match value {
|
||||
Value::Nil => Ok(None),
|
||||
Value::String(s) => {
|
||||
let os_str = lua_string_2_os_string(&s)?;
|
||||
ensure_no_nul(&context, &os_str)?;
|
||||
Ok(Some(PathBuf::from(os_str)))
|
||||
}
|
||||
other => Err(Error::TypeMismatch {
|
||||
field: context.to_string(),
|
||||
expected: "有效路径字符串 (LuaString) 或 nil",
|
||||
actual: other.type_name(),
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
/// 领域转换直接产出 crate::Error,不再被 mlua 包一层
|
||||
#[test]
|
||||
fn scalar_target_with_wrong_type_is_domain_error() {
|
||||
let err = Target::try_from(TargetDto::Scalar(Value::Integer(42))).unwrap_err();
|
||||
match err {
|
||||
Error::TypeMismatch { field, actual, .. } => {
|
||||
assert_eq!(field, "target");
|
||||
assert_eq!(actual, "integer");
|
||||
}
|
||||
other => panic!("unexpected error: {other}"),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn versioned_target_requires_home() {
|
||||
let dto = TargetDto::Versioned {
|
||||
home: Value::Nil,
|
||||
version_file: Value::Nil,
|
||||
current: Value::Nil,
|
||||
};
|
||||
let err = Target::try_from(dto).unwrap_err();
|
||||
assert!(matches!(err, Error::MissingField { field } if field.starts_with("home")));
|
||||
}
|
||||
|
||||
/// 字段不存在和字段是 nil,在 Lua 里是同一件事
|
||||
#[test]
|
||||
fn nil_target_is_missing_field() {
|
||||
let err = Target::try_from(TargetDto::Scalar(Value::Nil)).unwrap_err();
|
||||
assert!(matches!(err, Error::MissingField { .. }));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn optional_field_type_is_checked() {
|
||||
let err = optional_string(Value::Integer(1), "target.version_file").unwrap_err();
|
||||
assert!(matches!(err, Error::TypeMismatch { field, .. } if field == "target.version_file"));
|
||||
|
||||
let err = optional_path(Value::Integer(1), "target.current").unwrap_err();
|
||||
assert!(matches!(err, Error::TypeMismatch { field, .. } if field == "target.current"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn missing_target_is_missing_field() {
|
||||
let dto = TargetSpecDto {
|
||||
target: TargetDto::Scalar(Value::Nil),
|
||||
args: Value::Nil,
|
||||
env: Value::Nil,
|
||||
aliases: Value::Nil,
|
||||
};
|
||||
let err = TargetSpec::try_from(dto).unwrap_err();
|
||||
assert!(matches!(err, Error::MissingField { .. }));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,146 @@
|
||||
//! 名称规则:注册表键(命令名)、环境变量名、别名键。
|
||||
//!
|
||||
//! 三类名称的规则不同:
|
||||
//! - 命令名:非空、不能含空白字符、不能含 `=`、不能含 NUL
|
||||
//! - 环境变量名 / 别名:非空、不能含 `=`、不能含 NUL(允许空格)
|
||||
|
||||
use crate::{Error, NameKind, Result};
|
||||
use mlua::Value;
|
||||
|
||||
/// 校验注册表键(命令名)
|
||||
pub fn parse_command_name(name: &Value) -> Result<String> {
|
||||
let name = parse_string(name, NameKind::Command)?;
|
||||
|
||||
if name.trim().is_empty() {
|
||||
return Err(invalid(NameKind::Command, "不能为空字符串"));
|
||||
}
|
||||
if name.contains(|c: char| c.is_whitespace()) {
|
||||
return Err(invalid(
|
||||
NameKind::Command,
|
||||
format!("[{name}] 不能包含空格或空白字符"),
|
||||
));
|
||||
}
|
||||
if name.contains('=') {
|
||||
return Err(invalid(NameKind::Command, format!("[{name}] 不能包含 '='")));
|
||||
}
|
||||
if name.contains('\0') {
|
||||
return Err(invalid(
|
||||
NameKind::Command,
|
||||
format!("[{name}] 不能包含 NUL 字符"),
|
||||
));
|
||||
}
|
||||
Ok(name)
|
||||
}
|
||||
|
||||
/// 校验环境变量名 / 别名键
|
||||
pub(crate) fn parse_name(kind: NameKind, name: &Value) -> Result<String> {
|
||||
let name = parse_string(name, kind)?;
|
||||
|
||||
if name.is_empty() {
|
||||
return Err(invalid(kind, "不能为空"));
|
||||
}
|
||||
if name.contains('=') {
|
||||
return Err(invalid(kind, format!("[{name}] 不能包含 '='")));
|
||||
}
|
||||
if name.contains('\0') {
|
||||
return Err(invalid(kind, format!("[{name}] 不能包含 NUL 字符")));
|
||||
}
|
||||
Ok(name)
|
||||
}
|
||||
|
||||
/// 取字符串值;类型不对报 `TypeMismatch`,字节不是 UTF-8 报 `EncodingError`
|
||||
fn parse_string(value: &Value, kind: NameKind) -> Result<String> {
|
||||
match value {
|
||||
Value::String(s) => {
|
||||
s.to_str()
|
||||
.map(|text| text.to_string())
|
||||
.map_err(|e| Error::EncodingError {
|
||||
from: "LuaString",
|
||||
to: "String".to_string(),
|
||||
message: format!("{kind}不是合法的 UTF-8 字符串: {e}"),
|
||||
})
|
||||
}
|
||||
other => Err(Error::TypeMismatch {
|
||||
field: kind.to_string(),
|
||||
expected: "字符串 (String)",
|
||||
actual: other.type_name(),
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
fn invalid(kind: NameKind, reason: impl Into<String>) -> Error {
|
||||
Error::InvalidName {
|
||||
kind,
|
||||
reason: reason.into(),
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use mlua::Lua;
|
||||
|
||||
fn string(lua: &Lua, text: &str) -> Value {
|
||||
Value::String(lua.create_string(text).unwrap())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn command_name_accepts_plain_name() {
|
||||
let lua = Lua::new();
|
||||
assert_eq!(parse_command_name(&string(&lua, "git")).unwrap(), "git");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn command_name_rejects_whitespace_and_equals() {
|
||||
let lua = Lua::new();
|
||||
for bad in ["git status", "git=1", " ", ""] {
|
||||
let err = parse_command_name(&string(&lua, bad)).unwrap_err();
|
||||
assert!(
|
||||
matches!(
|
||||
err,
|
||||
Error::InvalidName {
|
||||
kind: NameKind::Command,
|
||||
..
|
||||
}
|
||||
),
|
||||
"unexpected error for {bad:?}: {err}"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn command_name_type_error_is_type_mismatch() {
|
||||
let err = parse_command_name(&Value::Integer(1)).unwrap_err();
|
||||
assert!(matches!(err, Error::TypeMismatch { .. }));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn env_name_allows_space_but_rejects_equals() {
|
||||
let lua = Lua::new();
|
||||
assert_eq!(
|
||||
parse_name(NameKind::EnvVar, &string(&lua, "MY VAR")).unwrap(),
|
||||
"MY VAR"
|
||||
);
|
||||
let err = parse_name(NameKind::EnvVar, &string(&lua, "FOO=1")).unwrap_err();
|
||||
assert!(matches!(
|
||||
err,
|
||||
Error::InvalidName {
|
||||
kind: NameKind::EnvVar,
|
||||
..
|
||||
}
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn alias_name_rejects_empty() {
|
||||
let lua = Lua::new();
|
||||
let err = parse_name(NameKind::Alias, &string(&lua, "")).unwrap_err();
|
||||
assert!(matches!(
|
||||
err,
|
||||
Error::InvalidName {
|
||||
kind: NameKind::Alias,
|
||||
..
|
||||
}
|
||||
));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,172 @@
|
||||
use super::as_table;
|
||||
use super::scalar::{ScalarMode, to_os_strings};
|
||||
use super::sequence::as_strict_sequence;
|
||||
use crate::config::naming::parse_name;
|
||||
use crate::{Error, NameKind, Result};
|
||||
use mlua::Value;
|
||||
use std::collections::HashMap;
|
||||
use std::ffi::OsString;
|
||||
|
||||
const CONTEXT: &str = "命令行别名 (aliases)";
|
||||
/// 数组嵌套上限,防止畸形配置打爆调用栈
|
||||
const MAX_DEPTH: usize = 16;
|
||||
/// `mr:` 引用链的最大深度
|
||||
const MAX_ALIAS_DEPTH: usize = 32;
|
||||
|
||||
/// `aliases = { name = value }` → 已展开的别名表。
|
||||
///
|
||||
/// value 是"命令行片段":字符串按 shell 规则分词,数组逐项处理。
|
||||
/// `mr:other` 在加载期递归展开;引用不存在的别名时原样保留;循环引用报错。
|
||||
pub(crate) fn parse_aliases(value: &Value) -> Result<HashMap<String, Vec<OsString>>> {
|
||||
let table = as_table(value, CONTEXT)?;
|
||||
let mut raw: HashMap<String, Vec<OsString>> = HashMap::with_capacity(table.raw_len().min(128));
|
||||
|
||||
for pair in table.pairs::<Value, Value>() {
|
||||
let (raw_key, raw_value) = pair?;
|
||||
let name = parse_name(NameKind::Alias, &raw_key)?;
|
||||
let context = format!("别名 '{name}'");
|
||||
|
||||
let mut parts = Vec::new();
|
||||
flatten(&raw_value, &context, 0, &mut parts)?;
|
||||
raw.insert(name, parts);
|
||||
}
|
||||
|
||||
expand_graph(&raw)
|
||||
}
|
||||
|
||||
/// 递归展平标量与嵌套数组,字符串按 shell 规则分词
|
||||
fn flatten(value: &Value, context: &str, depth: usize, out: &mut Vec<OsString>) -> Result<()> {
|
||||
if depth > MAX_DEPTH {
|
||||
return Err(Error::ParseError(format!(
|
||||
"{context} 的数组嵌套层级过深(超过 {MAX_DEPTH} 层)"
|
||||
)));
|
||||
}
|
||||
match value {
|
||||
Value::Table(table) => {
|
||||
for item in as_strict_sequence(table, context)? {
|
||||
flatten(&item, context, depth + 1, out)?;
|
||||
}
|
||||
}
|
||||
scalar => out.extend(to_os_strings(scalar, ScalarMode::Tokens, context)?),
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// 一次 DFS 完成"环检测 + 递归展开 + 结果缓存"
|
||||
fn expand_graph(raw: &HashMap<String, Vec<OsString>>) -> Result<HashMap<String, Vec<OsString>>> {
|
||||
let mut cache: HashMap<String, Vec<OsString>> = HashMap::with_capacity(raw.len());
|
||||
let mut stack: Vec<String> = Vec::new();
|
||||
|
||||
for key in raw.keys() {
|
||||
resolve(key, raw, &mut cache, &mut stack)?;
|
||||
}
|
||||
Ok(cache)
|
||||
}
|
||||
|
||||
fn resolve(
|
||||
key: &str,
|
||||
raw: &HashMap<String, Vec<OsString>>,
|
||||
cache: &mut HashMap<String, Vec<OsString>>,
|
||||
stack: &mut Vec<String>,
|
||||
) -> Result<Vec<OsString>> {
|
||||
if let Some(done) = cache.get(key) {
|
||||
return Ok(done.clone());
|
||||
}
|
||||
if stack.iter().any(|pending| pending == key) {
|
||||
return Err(Error::AliasCycle {
|
||||
name: key.to_string(),
|
||||
});
|
||||
}
|
||||
if stack.len() >= MAX_ALIAS_DEPTH {
|
||||
return Err(Error::AliasTooDeep {
|
||||
max: MAX_ALIAS_DEPTH,
|
||||
});
|
||||
}
|
||||
|
||||
stack.push(key.to_string());
|
||||
|
||||
let mut expanded = Vec::new();
|
||||
if let Some(args) = raw.get(key) {
|
||||
for arg in args {
|
||||
let text = arg.to_string_lossy();
|
||||
let referenced = text
|
||||
.strip_prefix("mr:")
|
||||
.filter(|name| raw.contains_key(*name))
|
||||
.map(str::to_string);
|
||||
|
||||
match referenced {
|
||||
Some(name) => expanded.extend(resolve(&name, raw, cache, stack)?),
|
||||
None => expanded.push(arg.clone()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
stack.pop();
|
||||
cache.insert(key.to_string(), expanded.clone());
|
||||
Ok(expanded)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use mlua::Lua;
|
||||
|
||||
fn with_aliases(code: &str) -> Result<HashMap<String, Vec<OsString>>> {
|
||||
let lua = Lua::new();
|
||||
let value: Value = lua.load(code).eval().unwrap();
|
||||
parse_aliases(&value)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn tokenizes_values_and_expands_references() {
|
||||
let aliases = with_aliases(
|
||||
r#"return { base = "log --graph", all = { "mr:base", "--all" }, blank = " " }"#,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(
|
||||
aliases.get("base").unwrap(),
|
||||
&vec![OsString::from("log"), OsString::from("--graph")]
|
||||
);
|
||||
assert_eq!(
|
||||
aliases.get("all").unwrap(),
|
||||
&vec![
|
||||
OsString::from("log"),
|
||||
OsString::from("--graph"),
|
||||
OsString::from("--all"),
|
||||
]
|
||||
);
|
||||
assert!(aliases.get("blank").unwrap().is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn scalar_number_and_boolean_are_supported() {
|
||||
let aliases = with_aliases(r#"return { n = 10, b = true }"#).unwrap();
|
||||
assert_eq!(aliases.get("n").unwrap(), &vec![OsString::from("10")]);
|
||||
assert_eq!(aliases.get("b").unwrap(), &vec![OsString::from("true")]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn unknown_reference_is_kept_literal() {
|
||||
let aliases = with_aliases(r#"return { a = "mr:nope" }"#).unwrap();
|
||||
assert_eq!(aliases.get("a").unwrap(), &vec![OsString::from("mr:nope")]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cycle_is_reported() {
|
||||
let err = with_aliases(r#"return { a = "mr:b", b = "mr:a" }"#).unwrap_err();
|
||||
assert!(matches!(err, Error::AliasCycle { .. }), "unexpected: {err}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn invalid_alias_name_is_reported() {
|
||||
let err = with_aliases(r#"return { [""] = "x" }"#).unwrap_err();
|
||||
assert!(matches!(
|
||||
err,
|
||||
Error::InvalidName {
|
||||
kind: NameKind::Alias,
|
||||
..
|
||||
}
|
||||
));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
use super::as_table;
|
||||
use super::scalar::{ScalarMode, to_os_strings};
|
||||
use super::sequence::as_strict_sequence;
|
||||
use crate::{Error, Result};
|
||||
use mlua::Value;
|
||||
use std::ffi::OsString;
|
||||
|
||||
const CONTEXT: &str = "命令行参数 (args)";
|
||||
|
||||
/// `args = { … }`:一个数组元素就是一个参数(空串保留为空参数)。
|
||||
///
|
||||
/// 只支持一维基础标量:嵌套 table 会让"一个元素 = 一个参数"的语义失效。
|
||||
pub(crate) fn parse_args(value: &Value) -> Result<Vec<OsString>> {
|
||||
let table = as_table(value, CONTEXT)?;
|
||||
let items = as_strict_sequence(table, CONTEXT)?;
|
||||
|
||||
let mut args = Vec::with_capacity(items.len());
|
||||
for (i, item) in items.iter().enumerate() {
|
||||
if matches!(item, Value::Table(_)) {
|
||||
return Err(Error::ParseError(format!(
|
||||
"{CONTEXT} 仅支持一维数组,第 {} 个元素不能是 table",
|
||||
i + 1
|
||||
)));
|
||||
}
|
||||
let context = format!("{CONTEXT} 第 {} 个元素", i + 1);
|
||||
args.extend(to_os_strings(item, ScalarMode::Verbatim, &context)?);
|
||||
}
|
||||
Ok(args)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use mlua::Lua;
|
||||
|
||||
fn with_args(code: &str) -> Result<Vec<OsString>> {
|
||||
let lua = Lua::new();
|
||||
let value: Value = lua.load(code).eval().unwrap();
|
||||
parse_args(&value)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parses_scalars_and_keeps_empty_string() {
|
||||
let args = with_args(r#"return { "--no-pager", 2, true, "" }"#).unwrap();
|
||||
assert_eq!(
|
||||
args,
|
||||
vec![
|
||||
OsString::from("--no-pager"),
|
||||
OsString::from("2"),
|
||||
OsString::from("true"),
|
||||
OsString::from(""),
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn empty_array_is_empty() {
|
||||
assert!(with_args("return {}").unwrap().is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rejects_nested_table() {
|
||||
let err = with_args(r#"return { { "a" } }"#).unwrap_err();
|
||||
assert!(matches!(err, Error::ParseError(_)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rejects_sparse_array() {
|
||||
let err = with_args(r#"return { "a", nil, "b" }"#).unwrap_err();
|
||||
assert!(matches!(err, Error::ParseError(_)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rejects_non_table() {
|
||||
let err = with_args(r#"return "-B""#).unwrap_err();
|
||||
assert!(matches!(err, Error::TypeMismatch { .. }));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,120 @@
|
||||
use super::as_table;
|
||||
use super::scalar::{ScalarMode, to_os_strings};
|
||||
use super::sequence::as_strict_sequence;
|
||||
use crate::config::naming::parse_name;
|
||||
use crate::{Error, NameKind, Result};
|
||||
use mlua::Value;
|
||||
use std::collections::HashMap;
|
||||
use std::ffi::OsString;
|
||||
|
||||
const CONTEXT: &str = "环境变量 (env)";
|
||||
/// 数组嵌套上限,防止畸形配置打爆调用栈
|
||||
const MAX_DEPTH: usize = 16;
|
||||
|
||||
/// `env = { KEY = value }` → 环境变量表。
|
||||
///
|
||||
/// value 可以是标量,也可以是任意深度的数组;数组会递归展平,然后按系统
|
||||
/// 路径分隔符(Windows `;`)拼接。空字符串 / 空数组 → 空值(清空该变量)。
|
||||
pub(crate) fn parse_env(value: &Value) -> Result<HashMap<String, OsString>> {
|
||||
let table = as_table(value, CONTEXT)?;
|
||||
let mut env = HashMap::with_capacity(table.raw_len().min(128));
|
||||
|
||||
for pair in table.pairs::<Value, Value>() {
|
||||
let (raw_key, raw_value) = pair?;
|
||||
let name = parse_name(NameKind::EnvVar, &raw_key)?;
|
||||
let context = format!("环境变量 '{name}' 的值");
|
||||
|
||||
let mut parts = Vec::new();
|
||||
flatten(&raw_value, &context, 0, &mut parts)?;
|
||||
|
||||
let joined = std::env::join_paths(parts)
|
||||
.map_err(|e| Error::ParseError(format!("{context} 无法用系统路径分隔符拼接: {e}")))?;
|
||||
env.insert(name, joined);
|
||||
}
|
||||
|
||||
Ok(env)
|
||||
}
|
||||
|
||||
/// 递归展平标量与嵌套数组(env 的字符串不做分词,整段作为一个路径段)
|
||||
fn flatten(value: &Value, context: &str, depth: usize, out: &mut Vec<OsString>) -> Result<()> {
|
||||
if depth > MAX_DEPTH {
|
||||
return Err(Error::ParseError(format!(
|
||||
"{context} 的数组嵌套层级过深(超过 {MAX_DEPTH} 层)"
|
||||
)));
|
||||
}
|
||||
match value {
|
||||
Value::Table(table) => {
|
||||
for item in as_strict_sequence(table, context)? {
|
||||
flatten(&item, context, depth + 1, out)?;
|
||||
}
|
||||
}
|
||||
scalar => out.extend(to_os_strings(scalar, ScalarMode::Verbatim, context)?),
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use mlua::Lua;
|
||||
|
||||
fn with_env(code: &str) -> Result<HashMap<String, OsString>> {
|
||||
let lua = Lua::new();
|
||||
let value: Value = lua.load(code).eval().unwrap();
|
||||
parse_env(&value)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn flattens_nested_arrays_with_path_separator() {
|
||||
let env = with_env(r#"return { PATH = { "C:/a", { "C:/b", "C:/c" } } }"#).unwrap();
|
||||
assert_eq!(env.get("PATH").unwrap().to_str(), Some("C:/a;C:/b;C:/c"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn scalars_and_empty_values() {
|
||||
let env = with_env(r#"return { A = "", B = {}, C = 3, D = true }"#).unwrap();
|
||||
assert_eq!(env.get("A").unwrap().to_str(), Some(""));
|
||||
assert_eq!(env.get("B").unwrap().to_str(), Some(""));
|
||||
assert_eq!(env.get("C").unwrap().to_str(), Some("3"));
|
||||
assert_eq!(env.get("D").unwrap().to_str(), Some("true"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rejects_invalid_key() {
|
||||
let err = with_env(r#"return { ["FOO=1"] = "x" }"#).unwrap_err();
|
||||
assert!(matches!(
|
||||
err,
|
||||
Error::InvalidName {
|
||||
kind: NameKind::EnvVar,
|
||||
..
|
||||
}
|
||||
));
|
||||
|
||||
let err = with_env(r#"return { [""] = "x" }"#).unwrap_err();
|
||||
assert!(matches!(
|
||||
err,
|
||||
Error::InvalidName {
|
||||
kind: NameKind::EnvVar,
|
||||
..
|
||||
}
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rejects_sparse_array_and_wrong_types() {
|
||||
let err = with_env(r#"return { P = { "a", nil, "b" } }"#).unwrap_err();
|
||||
assert!(matches!(err, Error::ParseError(_)));
|
||||
|
||||
let err = with_env(r#"return { F = function() end }"#).unwrap_err();
|
||||
assert!(matches!(err, Error::TypeMismatch { .. }));
|
||||
|
||||
let err = with_env(r#"return "PATH=C:/x""#).unwrap_err();
|
||||
assert!(matches!(err, Error::TypeMismatch { .. }));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rejects_nul_in_value() {
|
||||
let err = with_env(r#"return { P = { string.char(0) } }"#).unwrap_err();
|
||||
assert!(matches!(err, Error::ContainsNul(_)));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
//! Lua 值 → Rust 值的转换:`args` / `env` / `aliases`。
|
||||
//!
|
||||
//! 这一层只管"值本身":数组形状、元素类型、键合法性、分词与拼接。
|
||||
//! 产出的错误统一是 `crate::Error` 的结构化变体;字段级语义
|
||||
//! (必填、默认值)由 `model.rs` 决定。
|
||||
|
||||
mod aliases;
|
||||
mod args;
|
||||
mod env;
|
||||
mod scalar;
|
||||
mod sequence;
|
||||
|
||||
pub(crate) use aliases::parse_aliases;
|
||||
pub(crate) use args::parse_args;
|
||||
pub(crate) use env::parse_env;
|
||||
|
||||
use crate::{Error, Result};
|
||||
use mlua::{Table, Value};
|
||||
|
||||
/// 值必须是 table,否则报 `TypeMismatch`
|
||||
pub(crate) fn as_table<'a>(value: &'a Value, context: &str) -> Result<&'a Table> {
|
||||
value.as_table().ok_or_else(|| Error::TypeMismatch {
|
||||
field: context.to_string(),
|
||||
expected: "键值表 (table)",
|
||||
actual: value.type_name(),
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
use crate::args::tokens::parse_tokens;
|
||||
use crate::config::convert::{ensure_no_nul, lua_string_2_os_string};
|
||||
use crate::{Error, Result};
|
||||
use mlua::Value;
|
||||
use std::ffi::OsString;
|
||||
|
||||
/// 字符串元素的处理策略
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub(super) enum ScalarMode {
|
||||
/// 整体作为一个参数(args:一个数组元素 = 一个参数,空串保留为空参数)
|
||||
Verbatim,
|
||||
/// 按 shell 规则分词(aliases:"status -s" → 两个参数)
|
||||
Tokens,
|
||||
}
|
||||
|
||||
/// 标量 → 参数列表
|
||||
///
|
||||
/// - `Nil` 产生 0 个参数(在 env 里表示"清空该变量")
|
||||
/// - 字符串 / 整数 / 浮点 / 布尔都可以转成字符串
|
||||
/// - 其它类型报 `TypeMismatch`
|
||||
/// - 产出的每个 `OsString` 都会做 NUL 检查
|
||||
pub(super) fn to_os_strings(
|
||||
value: &Value,
|
||||
mode: ScalarMode,
|
||||
context: &str,
|
||||
) -> Result<Vec<OsString>> {
|
||||
let os_str: OsString = match value {
|
||||
Value::String(s) => lua_string_2_os_string(s)?,
|
||||
Value::Integer(i) => i.to_string().into(),
|
||||
Value::Number(n) => n.to_string().into(),
|
||||
Value::Boolean(b) => b.to_string().into(),
|
||||
Value::Nil => return Ok(Vec::new()),
|
||||
other => {
|
||||
return Err(Error::TypeMismatch {
|
||||
field: context.to_string(),
|
||||
expected: "string / number / boolean",
|
||||
actual: other.type_name(),
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
ensure_no_nul(&context, &os_str)?;
|
||||
|
||||
Ok(match mode {
|
||||
ScalarMode::Verbatim => vec![os_str],
|
||||
ScalarMode::Tokens => match os_str.to_str() {
|
||||
Some(text) => parse_tokens(text),
|
||||
// 非 UTF-8 字节没法按 shell 规则切分,整体作为一个参数
|
||||
None => vec![os_str],
|
||||
},
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
use crate::{Error, Result};
|
||||
use mlua::{Table, Value};
|
||||
|
||||
/// 严格连续的纯数组校验:一次遍历 `pairs`,要求键恰好是 `1..=n` 的连续整数。
|
||||
///
|
||||
/// 返回按顺序排列的元素。空洞(`{ "a", nil, "b" }`)或字典键都会报错。
|
||||
pub(super) fn as_strict_sequence(table: &Table, context: &str) -> Result<Vec<Value>> {
|
||||
let mut slots: Vec<Option<Value>> = Vec::with_capacity(table.raw_len().min(128));
|
||||
|
||||
for pair in table.pairs::<Value, Value>() {
|
||||
let (key, value) = pair?;
|
||||
let index = match key {
|
||||
Value::Integer(i) if i >= 1 => (i - 1) as usize,
|
||||
other => {
|
||||
return Err(Error::TypeMismatch {
|
||||
field: context.to_string(),
|
||||
expected: "连续整数下标 (数组)",
|
||||
actual: other.type_name(),
|
||||
});
|
||||
}
|
||||
};
|
||||
if slots.len() <= index {
|
||||
slots.resize_with(index + 1, || None);
|
||||
}
|
||||
slots[index] = Some(value);
|
||||
}
|
||||
|
||||
let mut items = Vec::with_capacity(slots.len());
|
||||
for (i, slot) in slots.into_iter().enumerate() {
|
||||
match slot {
|
||||
Some(value) => items.push(value),
|
||||
None => {
|
||||
return Err(Error::ParseError(format!(
|
||||
"{context} 第 {} 个元素为 nil 或不存在(请检查漏写引号或变量未定义)",
|
||||
i + 1
|
||||
)));
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(items)
|
||||
}
|
||||
Reference in New Issue
Block a user