use std::path::Path;
use crate::asset_api::{self, AssetRequest};
use crate::ecs::asset_id;
use super::errors_to_io;
pub fn validate_asset(
asset_type: &str,
name: &str,
args: &serde_json::Value,
) -> Result<(), String> {
asset_id::reset_interner();
crate::resource_handles::reset_resource_handles();
let type_norm = asset_type.to_lowercase().replace('_', "");
if matches!(
type_norm.as_str(),
"environment"
| "lightrig"
| "materialpalette"
| "camerashot"
| "prefab"
| "sceneimport"
| "characterschema"
| "charactermodel"
) {
return Ok(());
}
if crate::registry::RegisteredType::parse(asset_type).is_some_and(|t| t.is_resource()) {
crate::check::check_asset(&type_norm, name, args)?;
return Ok(());
}
let req = AssetRequest {
asset_type: asset_type.to_string(),
args: Some(args.clone()),
};
asset_api::create_asset_def(&req).map_err(|e| format!("Asset '{}': {}", name, e))?;
crate::check::check_asset(&type_norm, name, args)?;
Ok(())
}
pub fn validate_world_jsonl(content: &str, assets_dir: Option<&Path>) -> std::io::Result<()> {
let loaded = crate::build_only::prepare_world(content, assets_dir).map_err(errors_to_io)?;
let mut errors: Vec<String> = Vec::new();
for asset in &loaded.assets {
if crate::registry::RegisteredType::parse(&asset.asset_type)
.is_some_and(|t| t.is_resource())
{
continue;
}
let req = AssetRequest {
asset_type: asset.asset_type.clone(),
args: Some(asset.args.clone()),
};
if let Err(e) = asset_api::create_asset_def(&req) {
errors.push(format!("Asset '{}': {}", asset.name, e));
}
}
if errors.is_empty() {
Ok(())
} else {
Err(errors_to_io(errors))
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn visual_novel_world_validates() {
let world = r#"{"name":"gfx","type":"GraphicsConfig","args":{}}
{"name":"f","type":"Font","args":{"size_px":20}}
{"name":"title_menu","type":"Screen","args":{"initial":true}}
{"name":"title_menu_bg","type":"Sprite","args":{"x":0,"y":0,"width":640,"height":360,"tint":[0.1,0.1,0.1,1]}}
{"name":"title_menu_lbl","type":"TextLabel","args":{"font":"f","content":"Start","x":260,"y":160}}
{"name":"title_menu_btn","type":"HitRegion","args":{"x":260,"y":156,"width":120,"height":40,"label":"title_menu_lbl","action":"screen:show:vn_page_1"}}
{"name":"vn_page_1","type":"Screen","args":{}}
{"name":"vn_page_1_text","type":"TextLabel","args":{"font":"f","content":"hello","x":40,"y":40}}
{"name":"vn_page_1_next","type":"HitRegion","args":{"x":0,"y":0,"width":640,"height":360,"action":"screen:show:title_menu"}}
{"name":"pause_menu","type":"Screen","args":{}}
{"name":"pause_menu_dim","type":"Sprite","args":{"x":0,"y":0,"width":640,"height":360,"tint":[0,0,0,0.6]}}
{"name":"esc","type":"KeyBinding","args":{"key":"Escape","action":"screen:toggle:pause_menu"}}
"#;
validate_world_jsonl(world, None).expect("visual_novel-shaped world should validate");
}
#[test]
fn validate_asset_accepts_build_time_expansion_types() {
for ty in [
"SceneImport",
"Environment",
"LightRig",
"Prefab",
"CharacterSchema",
"CharacterModel",
] {
validate_asset(ty, "x", &serde_json::json!({}))
.unwrap_or_else(|e| panic!("{ty} should validate: {e}"));
}
}
#[test]
fn validate_asset_routes_resource_only_types_past_the_component_registry() {
validate_asset("AudioClip", "clip", &serde_json::json!({"source": "a.wav"}))
.expect("a source-backed AudioClip validates");
let err = validate_asset("Texture", "tex", &serde_json::json!({"generator": "nope"}))
.expect_err("an unknown texture generator is rejected");
assert!(err.contains("nope"), "got: {err}");
}
#[test]
fn validate_asset_runs_the_structural_check_after_type_resolution() {
validate_asset("Scene", "day", &serde_json::json!({})).expect("a Scene validates");
let err = validate_asset("Prop", "empty_prop", &serde_json::json!({}))
.expect_err("a source-less Prop is rejected");
assert!(err.contains("empty_prop"), "got: {err}");
}
#[test]
fn validate_asset_unknown_type_mentions_the_asset_name() {
let err =
validate_asset("Bogus", "my_thing", &serde_json::json!({})).expect_err("unknown type");
assert!(err.contains("my_thing"), "got: {err}");
}
#[test]
fn validate_asset_bad_args_mention_the_asset_name() {
let err = validate_asset(
"ProceduralMesh",
"bad_mesh",
&serde_json::json!({"generator": 5}),
)
.expect_err("bad args");
assert!(err.contains("bad_mesh"), "got: {err}");
}
#[test]
fn validate_world_jsonl_collects_every_resolution_failure() {
let world = concat!(
r#"{"name":"first","type":"ProceduralMesh","args":{"generator":"box"}}"#,
"\n",
r#"{"name":"clip","type":"AudioClip","args":{"source":"a.wav"}}"#,
"\n",
);
validate_world_jsonl(world, None).expect("a resolvable world validates");
let bad = concat!(
r#"{"name":"t1","type":"PointLight","args":{"intensity":"soon"}}"#,
"\n",
r#"{"name":"t2","type":"PointLight","args":{"intensity":"later"}}"#,
"\n",
);
let err = validate_world_jsonl(bad, None).expect_err("mistyped args do not resolve");
let msg = err.to_string();
assert!(msg.contains("Asset 't1'"), "got: {msg}");
assert!(msg.contains("Asset 't2'"), "got: {msg}");
}
}