共享入参解析器补「入参必须是对象」的契约断言与文档

- optional_text/required_text/page 加 debug_assert!(arguments.is_object()):非对象时 Value::get 恒为 None,会把 NotObject 静默判成「没给字段」或默认分页
- 文档写明调用方必须先跑 ToolArgumentsRejection::check;所有现有调用点已满足,release 行为不变

验证:cargo test --bin genarrative-ai-game-creator-shell -- agent::direct_tool_bridge:: agent::direct_tools_mcp:: 72 passed / 0 failed;cargo fmt --check 通过
This commit is contained in:
2026-10-01 17:49:25 +08:00
parent b7d77053ea
commit 0e60ecb769
@@ -34,6 +34,8 @@ pub(crate) trait ToolTextField<E>: Copy {
}
/// 必填文本字段:缺省、`null`、空串都算空值。
///
/// 契约同 [`optional_text`]:调用方必须先确认入参是对象。
pub(crate) fn required_text<F: ToolTextField<E>, E>(
arguments: &Value,
field: F,
@@ -42,10 +44,18 @@ pub(crate) fn required_text<F: ToolTextField<E>, E>(
}
/// 可选文本字段:缺省或 `null` 视为未提供;给了就必须在该字段自己的边界内。
///
/// 调用方必须先用 `ToolArgumentsRejection::check` 确认入参是 JSON 对象:非对象时
/// `Value::get` 恒为 `None`,这里会静默当成「没给这个字段」,把 `NotObject` 判成 `empty`。
/// 所有调用点现在都先跑 `check`,`debug_assert` 只保证以后不会有人绕过。
pub(crate) fn optional_text<F: ToolTextField<E>, E>(
arguments: &Value,
field: F,
) -> Result<Option<String>, E> {
debug_assert!(
arguments.is_object(),
"optional_text 的调用方必须先跑 ToolArgumentsRejection::check"
);
let Some(value) = arguments.get(field.key()).filter(|value| !value.is_null()) else {
return Ok(None);
};
@@ -76,12 +86,19 @@ pub(crate) struct ToolPage {
///
/// 调用点必须保证 `default_limit <= max_limit`:否则省略 `limit` 的请求会拿一个本身就越界的
/// 默认值去撞上限、每次都失败。这里用 `debug_assert` 让配错在测试期就炸出来。
///
/// 同样要求调用方先确认入参是对象:非对象时 `offset`/`limit` 都会被当成没给,静默回落到默认
/// 分页,把 `NotObject` 掩盖掉。
pub(crate) fn page(
arguments: &Value,
default_limit: usize,
max_limit: usize,
max_offset: Option<usize>,
) -> Result<ToolPage, ToolPageRejection> {
debug_assert!(
arguments.is_object(),
"page 的调用方必须先跑 ToolArgumentsRejection::check"
);
debug_assert!(default_limit <= max_limit);
let offset = arguments
.get("offset")