资源 kind 只保留一个公开解析入口
- server-rs/crates/shared-contracts/src/game_creation_app/asset_kind.rs:删掉公开的 `from_str_or_unknown()`(`from_str_lossy` 的残留命名),严格匹配降级为私有 `match_canonical()`;`parse_with_context()` 成为唯一公开解析入口,`FromStr`、serde 与所有边界一律走它,认不出 canonical 值只有这一处收口并留痕。 - server-rs/crates/shared-contracts/src/game_creation_app/asset_kind.rs:词汇表用例改走唯一入口,并断言每个非 canonical 变体都留痕。 - apps/ai-game-creator-shell/src-tauri/src/project/manifest.rs:读侧回捞 legacy kind 时不再调用第二个解析函数,直接用唯一词汇表 `ALL` 判真值(`unknown` 本身是成员,自然跳过)。
This commit is contained in:
@@ -1988,9 +1988,11 @@ fn non_canonical_manifest_asset_kinds(payload: &str) -> Vec<(String, String, Str
|
||||
let Some(raw_kind) = asset.get("kind").and_then(serde_json::Value::as_str) else {
|
||||
continue;
|
||||
};
|
||||
if raw_kind.trim() == GameCreationAppAssetKind::Unknown.as_str()
|
||||
|| GameCreationAppAssetKind::from_str_or_unknown(raw_kind)
|
||||
!= GameCreationAppAssetKind::Unknown
|
||||
// 判据直接来自唯一词汇表(`ALL` 含字面 `unknown`),不再另开一个"纯解析"入口:
|
||||
// 严格解析入口只有 `parse_with_context`,这里只需要判真值、不需要再留痕。
|
||||
if GameCreationAppAssetKind::ALL
|
||||
.iter()
|
||||
.any(|kind| kind.as_str() == raw_kind)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -79,10 +79,9 @@ macro_rules! game_creation_app_asset_kinds {
|
||||
|
||||
/// 严格解析:只认 canonical 值,其余(含大小写、空白变体)一律 `Unknown`。
|
||||
///
|
||||
/// 反序列化必须保持不可失败(一份 legacy manifest 不该让整个项目读不出来),
|
||||
/// 所以这里是"认不出就 `Unknown`",而不是 `Err`;名字里的 `or_unknown` 只描述
|
||||
/// 这个收口,不代表任何归一化或兼容。
|
||||
pub fn from_str_or_unknown(value: &str) -> Self {
|
||||
/// 私有实现细节,**不是**解析入口:外部一律走 [`Self::parse_with_context`],
|
||||
/// 保证"认不出"这件事只在一个地方被收口、被留痕。
|
||||
fn match_canonical(value: &str) -> Self {
|
||||
let mut index = 0;
|
||||
while index < Self::ALL.len() {
|
||||
let candidate = Self::ALL[index];
|
||||
@@ -94,12 +93,14 @@ macro_rules! game_creation_app_asset_kinds {
|
||||
Self::Unknown
|
||||
}
|
||||
|
||||
/// 解析外部 manifest / API 字符串:严格解析,并在认不出 canonical 值时把原始串与
|
||||
/// 上下文交给已注册的 reporter(AGC 壳注册成 `app_log!`)。
|
||||
/// **唯一**的 kind 解析入口:严格等值匹配,认不出 canonical 值时把原始串与上下文
|
||||
/// 交给已注册的 reporter(AGC 壳注册成 `app_log!`)。
|
||||
///
|
||||
/// 不做 trim、不做大小写归一、不查别名表、不做迁移——认不出就收口成 `Unknown`,
|
||||
/// 而不是 `Err`:反序列化必须不可失败(一份 legacy manifest 不该让整个项目读不出来)。
|
||||
/// 字面 `"unknown"` 本身是合法输入,不算异常,不报。
|
||||
pub fn parse_with_context(value: &str, context: &'static str) -> Self {
|
||||
let parsed = Self::from_str_or_unknown(value);
|
||||
let parsed = Self::match_canonical(value);
|
||||
if parsed == Self::Unknown && value != Self::Unknown.as_str() {
|
||||
report_non_canonical_asset_kind(value, context);
|
||||
}
|
||||
@@ -328,6 +329,7 @@ mod tests {
|
||||
/// 严格口径下大小写与空白变体都不命中。
|
||||
#[test]
|
||||
fn declared_vocabulary_is_unique_and_strictly_parsed() {
|
||||
let _guard = record_reports();
|
||||
assert_eq!(GameCreationAppAssetKind::ALL.len(), 19);
|
||||
let mut wire_values = std::collections::BTreeSet::new();
|
||||
for kind in GameCreationAppAssetKind::ALL {
|
||||
@@ -337,20 +339,22 @@ mod tests {
|
||||
kind.as_str()
|
||||
);
|
||||
assert_eq!(
|
||||
GameCreationAppAssetKind::from_str_or_unknown(kind.as_str()),
|
||||
GameCreationAppAssetKind::parse_with_context(kind.as_str(), "unit-test.vocabulary"),
|
||||
*kind
|
||||
);
|
||||
}
|
||||
assert_eq!(
|
||||
GameCreationAppAssetKind::from_str_or_unknown("unknown"),
|
||||
GameCreationAppAssetKind::parse_with_context("unknown", "unit-test.vocabulary"),
|
||||
GameCreationAppAssetKind::Unknown
|
||||
);
|
||||
for raw in ["UI", "ui", " image ", "Image", "art-spritesheet-slice"] {
|
||||
assert_eq!(
|
||||
GameCreationAppAssetKind::from_str_or_unknown(raw),
|
||||
GameCreationAppAssetKind::parse_with_context(raw, "unit-test.vocabulary"),
|
||||
GameCreationAppAssetKind::Unknown,
|
||||
"{raw} 不许命中 canonical 值"
|
||||
);
|
||||
}
|
||||
// 每个非 canonical 变体都要留痕(5 条大小写/空白/legacy 变体都算异常)。
|
||||
assert_eq!(recorded_reports().len(), 5);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user