use crate::ecs::{AssetKind, AssetOrigin, BlobAssetDef};
use crate::registry::RegisteredType;
use crate::registry::Registration;
use crate::result::CnResult;
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
pub struct AssetRequest {
pub asset_type: String,
#[serde(default)]
pub args: Option<serde_json::Value>,
}
pub fn create_asset_def(req: &AssetRequest) -> Result<BlobAssetDef, CnResult> {
if let Some(ct) = RegisteredType::parse(&req.asset_type) {
let reg = ct.registration();
if reg.origin != AssetOrigin::External {
return Err(CnResult::InvalidArgument);
}
let discriminant = ct.discriminant().ok_or(CnResult::InvalidArgument)?;
let args = resolve_args(®, &req.args);
let args_bytes = match crate::registry::bake_divergent(ct, &args)? {
Some(bytes) => bytes,
None => ct.reserialize_args(&args)?,
};
return Ok(BlobAssetDef {
name: None,
kind: AssetKind::Component,
discriminant,
args_bytes,
payload: None,
});
}
tracing::error!("asset_api: unknown asset type '{}'", req.asset_type);
Err(CnResult::AssetInvalidType)
}
fn resolve_args(reg: &Registration, supplied: &Option<serde_json::Value>) -> serde_json::Value {
let mut base = reg
.default_args
.clone()
.unwrap_or_else(|| serde_json::Value::Object(Default::default()));
if let Some(serde_json::Value::Object(supplied_map)) = supplied {
if let serde_json::Value::Object(ref mut base_map) = base {
for (k, v) in supplied_map {
base_map.insert(k.clone(), v.clone());
}
}
} else if let Some(v) = supplied {
base = v.clone();
}
base
}
#[cfg(test)]
mod tests {
use super::*;
fn shader_reg() -> Registration {
Registration {
type_name: "VertexStage",
origin: AssetOrigin::External,
payload: crate::ecs::AssetPayload::Compiled,
default_args: Some(serde_json::json!({ "source": "user.metal" })),
}
}
#[test]
fn resolve_args_none_uses_default() {
let reg = shader_reg();
let result = resolve_args(®, &None);
assert_eq!(result["source"], "user.metal");
}
#[test]
fn resolve_args_empty_object_fills_from_default() {
let reg = shader_reg();
let supplied = Some(serde_json::json!({}));
let result = resolve_args(®, &supplied);
assert_eq!(result["source"], "user.metal");
}
#[test]
fn resolve_args_supplied_value_wins() {
let reg = shader_reg();
let supplied = Some(serde_json::json!({ "source": "custom.metal" }));
let result = resolve_args(®, &supplied);
assert_eq!(result["source"], "custom.metal");
}
#[test]
fn resolve_args_partial_keeps_default_for_missing_keys() {
let reg = Registration {
type_name: "Fake",
origin: AssetOrigin::External,
payload: crate::ecs::AssetPayload::None,
default_args: Some(serde_json::json!({ "a": 1, "b": 2 })),
};
let supplied = Some(serde_json::json!({ "b": 99 }));
let result = resolve_args(®, &supplied);
assert_eq!(result["a"], 1);
assert_eq!(result["b"], 99);
}
#[test]
fn resolve_args_non_object_supplied_replaces_the_default() {
let reg = shader_reg();
let supplied = Some(serde_json::json!([1, 2, 3]));
let result = resolve_args(®, &supplied);
assert_eq!(result, serde_json::json!([1, 2, 3]));
}
#[test]
fn resolve_args_keeps_a_non_object_default_when_an_object_is_supplied() {
let reg = Registration {
type_name: "Fake",
origin: AssetOrigin::External,
payload: crate::ecs::AssetPayload::None,
default_args: Some(serde_json::json!([1, 2, 3])),
};
let supplied = Some(serde_json::json!({ "a": 1 }));
assert_eq!(resolve_args(®, &supplied), serde_json::json!([1, 2, 3]));
}
#[test]
fn create_asset_def_rejects_unknown_types() {
let req = AssetRequest {
asset_type: "NotARealAsset".to_string(),
args: None,
};
assert_eq!(
create_asset_def(&req).unwrap_err(),
CnResult::AssetInvalidType
);
}
#[test]
fn create_asset_def_rejects_runtime_only_types() {
let req = AssetRequest {
asset_type: "Transform".to_string(),
args: None,
};
assert_eq!(
create_asset_def(&req).unwrap_err(),
CnResult::InvalidArgument
);
}
#[test]
fn create_asset_def_builds_a_component_def() {
let req = AssetRequest {
asset_type: "ProceduralMesh".to_string(),
args: None,
};
let def = create_asset_def(&req).unwrap();
assert_eq!(def.kind, AssetKind::Component);
assert_eq!(
Some(def.discriminant),
RegisteredType::parse("ProceduralMesh")
.unwrap()
.discriminant()
);
assert!(def.name.is_none());
assert!(def.payload.is_none());
postcard::from_bytes::<crate::components::ProceduralMesh>(&def.args_bytes).unwrap();
}
#[test]
fn every_addable_component_type_builds_a_baked_def() {
for (ct, _) in RegisteredType::addable_types().filter(|(t, _)| !t.is_resource()) {
let def = create_asset_def(&AssetRequest {
asset_type: ct.as_str().to_string(),
args: None,
})
.unwrap();
assert_eq!(def.kind, AssetKind::Component, "{}", ct.as_str());
assert!(!def.args_bytes.is_empty(), "{}", ct.as_str());
}
}
#[test]
fn create_asset_def_merges_supplied_args_over_defaults() {
let req = AssetRequest {
asset_type: "ProceduralMesh".to_string(),
args: Some(serde_json::json!({ "generator": "box" })),
};
let def = create_asset_def(&req).unwrap();
let baked: crate::components::ProceduralMesh =
postcard::from_bytes(&def.args_bytes).unwrap();
assert_eq!(baked.generator, "box");
let defaults = crate::components::ProceduralMesh::default();
assert_eq!(baked.half_width, defaults.half_width);
assert_eq!(baked.ceiling_height, defaults.ceiling_height);
}
#[test]
fn create_asset_def_rejects_mistyped_args() {
let req = AssetRequest {
asset_type: "ProceduralMesh".to_string(),
args: Some(serde_json::json!({ "generator": 42 })),
};
assert_eq!(
create_asset_def(&req).unwrap_err(),
CnResult::InvalidArgument
);
}
#[test]
fn addable_types_are_external_only() {
let names: Vec<&str> = RegisteredType::addable_types()
.inspect(|(_, reg)| assert_eq!(reg.origin, AssetOrigin::External))
.map(|(ct, _)| ct.as_str())
.collect();
assert!(!names.is_empty());
assert!(names.contains(&"ProceduralMesh"));
assert!(!names.contains(&"Transform"));
}
}